<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Johnson</title>
    <description>The latest articles on DEV Community by Johnson (@johnson998877).</description>
    <link>https://dev.to/johnson998877</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3839110%2Fed268e99-ea7b-4e48-ab54-420a4b5a3305.jpg</url>
      <title>DEV Community: Johnson</title>
      <link>https://dev.to/johnson998877</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/johnson998877"/>
    <language>en</language>
    <item>
      <title>Debugging Filestash 'Invalid Account': How Response Time Led Me to a Swapped Config Field</title>
      <dc:creator>Johnson</dc:creator>
      <pubDate>Sat, 04 Apr 2026 14:25:36 +0000</pubDate>
      <link>https://dev.to/johnson998877/debugging-filestash-invalid-account-how-response-time-led-me-to-a-swapped-config-field-1opi</link>
      <guid>https://dev.to/johnson998877/debugging-filestash-invalid-account-how-response-time-led-me-to-a-swapped-config-field-1opi</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Filestash's passthrough auth returns "Invalid account" on login. The backend is configured to forward credentials to the local SSH server (OpenSSH on the same machine). SSH works fine when tested directly. Password auth is enabled.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SYST INFO [auth] status=failed user=myuser backend=sftp err=Invalid+account
HTTP 307 POST  4.7ms /api/session/auth/?label=sftp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Environment:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filestash running in Docker (bridge network &lt;code&gt;172.20.0.0/16&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;SSH server on host: &lt;code&gt;192.168.1.x:22&lt;/code&gt;, listening on &lt;code&gt;0.0.0.0:22&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;UFW active on host&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;identity_provider.type = passthrough&lt;/code&gt;, &lt;code&gt;attribute_mapping.related_backend = sftp&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Diagnostic Step 1: Read the Response Time
&lt;/h2&gt;

&lt;p&gt;Before touching config, the response time is the first clue.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 5ms&lt;/td&gt;
&lt;td&gt;Pre-connection failure — params missing/invalid, DNS failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~5–50ms&lt;/td&gt;
&lt;td&gt;TCP connection refused (immediate RST)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100–500ms&lt;/td&gt;
&lt;td&gt;SSH handshake + auth failure (wrong password)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3000ms+&lt;/td&gt;
&lt;td&gt;TCP timeout (UFW DROP, no route)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;4.7ms → we never attempted a TCP connection.&lt;/strong&gt; The error is thrown before &lt;code&gt;ssh.Dial()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Filestash's Go SFTP backend (&lt;code&gt;plg_backend_sftp.go&lt;/code&gt;), &lt;code&gt;ErrNotValid&lt;/code&gt; ("Invalid account") is returned whenever:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;hostname&lt;/code&gt; is empty after decryption&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ssh.Dial()&lt;/code&gt; fails for any reason&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At 4.7ms, it's case 1.&lt;/p&gt;




&lt;h2&gt;
  
  
  Diagnostic Step 2: Test Container Network Access
&lt;/h2&gt;

&lt;p&gt;Even though the fast response time ruled out a network issue causing &lt;em&gt;this&lt;/em&gt; failure, understanding the network matters for the full fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From inside the Filestash container&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;filestash curl &lt;span class="nt"&gt;--connect-timeout&lt;/span&gt; 3 telnet://192.168.1.x:22
&lt;span class="c"&gt;# → Timed out after 3002ms&lt;/span&gt;

docker &lt;span class="nb"&gt;exec &lt;/span&gt;filestash curl &lt;span class="nt"&gt;--connect-timeout&lt;/span&gt; 3 telnet://172.20.0.1:22
&lt;span class="c"&gt;# → Timed out after 3002ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;UFW is blocking Docker's subnet from SSH.&lt;/strong&gt; This won't cause "Invalid account" (too slow), but it means even after fixing the config, we'll need to address networking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Diagnostic Step 3: Read the Actual Config
&lt;/h2&gt;

&lt;p&gt;The SFTP params are AES-256-GCM encrypted in &lt;code&gt;config.json&lt;/code&gt;. The admin API (&lt;code&gt;GET /admin/api/config&lt;/code&gt;) returns decrypted values — but only if you're the browser.&lt;/p&gt;

&lt;p&gt;Initial curl attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-b&lt;/span&gt; admin_cookies.txt http://localhost:8334/admin/api/config
&lt;span class="c"&gt;# → {"status": "error", "message": "Not Allowed"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the Filestash JS source (&lt;code&gt;lib/ajax.js&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Requested-With&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;XmlHttpRequest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All Filestash AJAX requests include this header. Without it, admin endpoints return 403.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-b&lt;/span&gt; admin_cookies.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-Requested-With: XmlHttpRequest'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://localhost:8334/admin/api/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the decrypted config returns. The SFTP params:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sftp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sftp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hostname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myserver"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ .user }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ .password }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192.168.1.x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"22"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;hostname&lt;/code&gt; = &lt;code&gt;"myserver"&lt;/code&gt; (machine hostname, not resolvable inside Docker container)&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;code&gt;path&lt;/code&gt; = &lt;code&gt;"192.168.1.x"&lt;/code&gt; (IP address, entered in the wrong field)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fields were swapped during browser configuration. DNS lookup for the machine hostname fails instantly inside the container → "Invalid account" at 4.7ms. ✅ Confirmed.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Two issues to address:&lt;/p&gt;
&lt;h3&gt;
  
  
  Fix 1: Correct the SFTP params
&lt;/h3&gt;

&lt;p&gt;POST the corrected config via admin API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;

&lt;span class="c1"&gt;# Read current admin config
&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;admin_cookies.txt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-H&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;X-Requested-With: XmlHttpRequest&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8334/admin/api/config&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# strip formObj metadata wrappers (Filestash admin API format)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;strip_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;strip_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;

&lt;span class="n"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;strip_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Fix the swapped fields
&lt;/span&gt;&lt;span class="n"&gt;sftp_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;middleware&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;attribute_mapping&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;sftp_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sftp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hostname&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;# SSH server on the host
&lt;/span&gt;&lt;span class="n"&gt;sftp_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sftp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/home/myuser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;    &lt;span class="c1"&gt;# initial directory
&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;middleware&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;attribute_mapping&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sftp_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Restore connections (they're not in admin config, come from public config)
&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;connections&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sftp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sftp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;

&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;admin_cookies.txt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-H&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;X-Requested-With: XmlHttpRequest&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-H&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Type: application/json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-X&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8334/admin/api/config&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Always restore &lt;code&gt;connections&lt;/code&gt; when POSTing admin config — the admin config endpoint does not include connections (they come from the public &lt;code&gt;/api/config&lt;/code&gt; endpoint). If you don't add them back, the login form disappears.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Fix 2: Fix container networking
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;hostname = 127.0.0.1&lt;/code&gt;, the container needs to reach the host's SSH. Two options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — UFW rule (requires sudo):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 172.25.0.0/16 to any port 22
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option B — Host networking (no sudo, cleaner):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;filestash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;filestash/filestash&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;filestash&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;        &lt;span class="c1"&gt;# remove ports: mapping, not needed with host networking&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;filestash_data:/app/data/state&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;filestash_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/filestash &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose down &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With host networking, the Filestash process runs on the host's network stack. &lt;code&gt;127.0.0.1:22&lt;/code&gt; is the host's SSH server. No UFW rules needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;After the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SYST INFO [auth] status=failed user=myuser backend=sftp err=Invalid+account
HTTP 307 POST  53ms /api/session/auth/?label=sftp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;53ms&lt;/strong&gt; — actual SSH connection attempted (test was with wrong password, so auth failed, but the connection was made). With correct credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SYST INFO [auth] status=ok user=myuser backend=sftp
HTTP 302 POST  91ms /api/session/auth/?label=sftp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hostname/path swapped&lt;/td&gt;
&lt;td&gt;"Invalid account" in 4.7ms&lt;/td&gt;
&lt;td&gt;Browser form filled incorrectly — &lt;code&gt;hostname="myserver"&lt;/code&gt;, &lt;code&gt;path="192.168.1.x"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Swap: &lt;code&gt;hostname="127.0.0.1"&lt;/code&gt;, &lt;code&gt;path="/home/myuser"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UFW blocks Docker subnet&lt;/td&gt;
&lt;td&gt;TCP timeout (3s) to host SSH&lt;/td&gt;
&lt;td&gt;UFW blocks &lt;code&gt;172.25.0.0/16 → port 22&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Switch to &lt;code&gt;network_mode: host&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key debugging insight:&lt;/strong&gt; Response time tells you where the failure is. Sub-5ms = before any network I/O. This rules out 90% of the usual suspects and points directly at config/param issues.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist for Filestash SFTP Passthrough Auth
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;middleware.identity_provider.type = "passthrough"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;middleware.attribute_mapping.related_backend = "sftp"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;attribute_mapping.params&lt;/code&gt; contains correct &lt;code&gt;hostname&lt;/code&gt; (not the machine's hostname, not the path)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;connections&lt;/code&gt; array is present in config (check &lt;code&gt;/api/config&lt;/code&gt; public endpoint)&lt;/li&gt;
&lt;li&gt;[ ] Container can reach &lt;code&gt;hostname:port&lt;/code&gt; via TCP (test with &lt;code&gt;curl telnet://host:port&lt;/code&gt; from inside container)&lt;/li&gt;
&lt;li&gt;[ ] SSH server has &lt;code&gt;PasswordAuthentication yes&lt;/code&gt; (or not explicitly disabled)&lt;/li&gt;
&lt;li&gt;[ ] When calling admin API from scripts: include &lt;code&gt;X-Requested-With: XmlHttpRequest&lt;/code&gt; header&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Filestash: &lt;a href="https://www.filestash.app" rel="noopener noreferrer"&gt;https://www.filestash.app&lt;/a&gt; — self-hosted file browser with SFTP, S3, FTP, and more.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>debugging</category>
      <category>homelab</category>
    </item>
    <item>
      <title>I Downloaded the Claude Code Source Before Anthropic DMCA'd It</title>
      <dc:creator>Johnson</dc:creator>
      <pubDate>Wed, 01 Apr 2026 06:50:56 +0000</pubDate>
      <link>https://dev.to/johnson998877/i-downloaded-the-claude-code-source-before-anthropic-dmcad-it-fll</link>
      <guid>https://dev.to/johnson998877/i-downloaded-the-claude-code-source-before-anthropic-dmcad-it-fll</guid>
      <description>&lt;p&gt;Someone reverse-engineered Anthropic's proprietary Claude Code CLI from its compiled npm bundle. They used Claude itself to do it. Published April 1st. 3.6k stars. 4.7k forks. In 15 hours.&lt;/p&gt;

&lt;p&gt;The author left a note in the README: &lt;em&gt;"I don't know how long this project will exist. Fork doesn't work well — git clone is more stable."&lt;/em&gt; Translation: DMCA imminent.&lt;/p&gt;

&lt;p&gt;I cloned it immediately. Spent several hours auditing 3,817 files across 62 source directories. Here's what's actually inside — and why this is different from any source leak you've seen before.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Isn't a Sourcemap Leak — It's Stranger
&lt;/h2&gt;

&lt;p&gt;When developers hear "source leak," they assume accidentally shipped &lt;code&gt;.map&lt;/code&gt; files alongside minified JS. The standard accidental disclosure. I checked the official &lt;code&gt;@anthropic-ai/claude-code&lt;/code&gt; npm package for exactly that — nothing. Anthropic shipped clean.&lt;/p&gt;

&lt;p&gt;What actually happened here is more interesting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decompile the &lt;strong&gt;25MB official npm bundle&lt;/strong&gt; using standard reverse tools&lt;/li&gt;
&lt;li&gt;Feed the decompiled output into &lt;strong&gt;4 rounds × 7 parallel Claude agents&lt;/strong&gt; to reconstruct TypeScript types, interfaces, and file structure&lt;/li&gt;
&lt;li&gt;The result had &lt;strong&gt;1,341 type errors&lt;/strong&gt; — not unusual for AI-reconstructed code&lt;/li&gt;
&lt;li&gt;Run another round of Claude agents specifically to fix type errors → &lt;strong&gt;zero remaining&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Auto-generate &lt;strong&gt;1,206 stubs&lt;/strong&gt; for Anthropic's private internal packages (the ones not in the npm bundle)&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;bun run dev&lt;/code&gt; — it &lt;strong&gt;boots&lt;/strong&gt;, connects to the real Anthropic API, and prints version &lt;code&gt;2.1.87&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They used Claude to rebuild Claude from Claude's own compiled output. The recursion is real.&lt;/p&gt;

&lt;p&gt;The repository includes a &lt;code&gt;RECORD.md&lt;/code&gt; file documenting the entire process, including the developer's machine path: &lt;code&gt;/Users/konghayao/code/ai/claude-code&lt;/code&gt;. The original author is &lt;code&gt;konghayao&lt;/code&gt;, and they documented every step of the methodology with enough detail that the reconstruction is reproducible.&lt;/p&gt;




&lt;h2&gt;
  
  
  30 Hidden Feature Flags Nobody Knew About
&lt;/h2&gt;

&lt;p&gt;This is the finding that stops you cold. Inside the entry point, a single polyfill replaces the entire feature-flagging system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;feature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every feature check in the codebase calls &lt;code&gt;feature("FLAG_NAME")&lt;/code&gt;. That one function controls all of them. And there are &lt;strong&gt;30 named flags&lt;/strong&gt; gating production-ready code that ships with every install — just silently disabled.&lt;/p&gt;

&lt;p&gt;These aren't placeholder stubs. They're connected to real UI flows, real API calls, real session management. The code is finished. The flag just says no.&lt;/p&gt;

&lt;p&gt;Here's what's waiting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KAIROS&lt;/strong&gt; — The always-running autonomous agent. Designed to operate indefinitely in the background. Sends push notifications when it needs a human decision, drops completed files when done, and resumes automatically. This is the "AI that works while you sleep."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DAEMON + BRIDGE_MODE&lt;/strong&gt; — Claude Code restructured as a persistent background service with a remote command interface. Clients connect to the daemon and issue commands. Multiple Claude instances on the same machine, all orchestrated through a single process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BG_SESSIONS&lt;/strong&gt; — Background session management with a full lifecycle API: &lt;code&gt;ps&lt;/code&gt; (list active sessions), &lt;code&gt;logs&lt;/code&gt; (stream output), &lt;code&gt;attach&lt;/code&gt; (drop in), &lt;code&gt;kill&lt;/code&gt; (terminate). tmux for AI agents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH_REMOTE&lt;/strong&gt; — &lt;code&gt;claude ssh &amp;lt;host&amp;gt;&lt;/code&gt;. Issue Claude commands directly to a remote machine. Not a terminal emulator. Claude Code itself, running headless on the remote server, controlled from your local terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FORK_SUBAGENT&lt;/strong&gt; — Mid-task state cloning. While Claude is doing something complex, you fork the current agent. The fork starts in the exact same state. Both continue independently. You can compare results or parallelize subtasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;COORDINATOR_MODE&lt;/strong&gt; — One Claude orchestrating a fleet of Claude workers. The coordinator breaks down work, assigns it, aggregates results. Multi-agent inside a single project, without you managing anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VOICE_MODE&lt;/strong&gt; — Push-to-talk integration. Already wired up. Just needs the flag flipped and a microphone to be useful. The UI code is there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KAIROS_GITHUB_WEBHOOKS&lt;/strong&gt; — Claude subscribes to your repository's PR events. Commit pushed? Review requested? Claude wakes up, processes the event, and takes configured actions in real time — without you opening a terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ULTRAPLAN&lt;/strong&gt; — An extended planning mode distinct from the current &lt;code&gt;plan&lt;/code&gt; mode. The source suggests it enables significantly longer-horizon task decomposition, likely for multi-session project work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WEB_BROWSER_TOOL&lt;/strong&gt; — A browsing capability directly inside Claude Code. Not scraping. A real browser tool, integrated into the agent's tool loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PROACTIVE&lt;/strong&gt; — Claude monitors context and responds to things without being asked. Detects patterns, flags issues, suggests actions before you request them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BUDDY&lt;/strong&gt; — Multi-instance collaborative mode. Two Claude instances working on the same codebase simultaneously, with session awareness of each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DIRECT_CONNECT&lt;/strong&gt; — Peer-to-peer tunnel between Claude Code instances. Skip the daemon; connect two agents directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TORCH&lt;/strong&gt; and &lt;strong&gt;LODESTONE&lt;/strong&gt; — Undocumented. Based on the code context, these appear to be infrastructure flags for internal Anthropic deployment tooling. The names suggest something directional — LODESTONE typically means a navigational reference point.&lt;/p&gt;

&lt;p&gt;None of this was on any roadmap. None of it was announced. All of it shipped with &lt;code&gt;@anthropic-ai/claude-code@2.1.87&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture Explains Why the Gap Is Unbridgeable
&lt;/h2&gt;

&lt;p&gt;The permission system alone is &lt;strong&gt;6,300 lines of TypeScript&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It includes a YOLO classifier — a component that evaluates whether a given shell command is safe to auto-execute without asking. It factors in command type, path context, recent session history, and risk heuristics. Then decides: ask the user, execute automatically, or block.&lt;/p&gt;

&lt;p&gt;The three operating modes (&lt;code&gt;plan&lt;/code&gt;, &lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;manual&lt;/code&gt;) aren't UI toggles. They're deeply wired into the execution loop with different code paths for each. Switching modes mid-session is a state transition, not a preference change.&lt;/p&gt;

&lt;p&gt;The streaming query loop is &lt;strong&gt;1,700 lines&lt;/strong&gt;. It simultaneously manages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Token budget tracking and soft/hard limit behavior&lt;/li&gt;
&lt;li&gt;Session auto-compaction when context fills (preserves working set, discards old context)&lt;/li&gt;
&lt;li&gt;Multi-tool call attribution (knows which tool call produced which output)&lt;/li&gt;
&lt;li&gt;Partial response streaming with backpressure&lt;/li&gt;
&lt;li&gt;Interrupt handling at the right mid-execution points&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every session is serialized to disk as a JSONL artifact. This single design decision enables everything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/resume&lt;/code&gt; brings back any session from any device&lt;/li&gt;
&lt;li&gt;Sessions survive machine reboots and crashes&lt;/li&gt;
&lt;li&gt;FORK_SUBAGENT works because the state is just a file — you copy it&lt;/li&gt;
&lt;li&gt;The QR code teleport feature (found in the source) works by sharing the session artifact path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are no open-source coding agents with this infrastructure. The closest alternatives have single-session architectures. Claude Code treats sessions as first-class long-lived objects.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Decompiled Comments Exposed
&lt;/h2&gt;

&lt;p&gt;Minification strips variable names and whitespace. It doesn't touch code comments. The 25MB bundle shipped with every comment that was present at compile time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6 internal Anthropic Slack channel links&lt;/strong&gt; — full &lt;code&gt;https://anthropic.slack.com/archives/CHANNEL_ID/p...&lt;/code&gt; permalinks preserved in comments. These are engineering discussion threads that the comment author left as references. The channel IDs are: &lt;code&gt;C07VBSHV7EV&lt;/code&gt;, &lt;code&gt;C06FE2FP0Q2&lt;/code&gt;, &lt;code&gt;C0AHK9P0129&lt;/code&gt;, &lt;code&gt;C093BJBD1CP&lt;/code&gt;, &lt;code&gt;C08428WSLKV&lt;/code&gt;, &lt;code&gt;C093UA0KLD7&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production OAuth CLIENT_ID:&lt;/strong&gt; &lt;code&gt;9d1c250a-e61b-44d9-88ed-5944d1962f5e&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Staging CLIENT_ID:&lt;/strong&gt; &lt;code&gt;22422756-60c9-4084-8eb7-27705fd5cf9a&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Staging infrastructure domains:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;platform.staging.ant.dev&lt;/code&gt; (Anthropic's internal shorthand domain for staging)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api-staging.anthropic.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;claude-ai.staging.ant.dev&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;US Federal Government instance:&lt;/strong&gt; &lt;code&gt;claude.fedstart.com&lt;/code&gt; — a separate Claude deployment for FedRAMP-compliant government customers. There's also &lt;code&gt;claude-staging.fedstart.com&lt;/code&gt; for the staging layer of that deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Private GitHub issues up to #30,912&lt;/strong&gt; — comments referencing internal GitHub issues confirm the private &lt;code&gt;anthropics/claude-code&lt;/code&gt; repository is enormous, with tens of thousands of internal issues the public never sees.&lt;/p&gt;

&lt;p&gt;Everything came from comment strings the minifier preserved verbatim.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Strategic Moat Is a Boolean
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;USER_TYPE&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// full feature access&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anthropic engineers run the exact same binary as every Pro or Max subscriber. Same npm package. Same version number. But with &lt;code&gt;USER_TYPE=ant&lt;/code&gt; set in the environment, every feature flag in that polyfill becomes &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They've been using KAIROS, DAEMON, and COORDINATOR_MODE internally for months — possibly longer. The code is mature. It has edge case handling, error recovery, retry logic. This isn't prototype code. It's been running in production for Anthropic's own engineers.&lt;/p&gt;

&lt;p&gt;When these features ship publicly, there's zero migration cost. The install artifact is identical. The flag just flips.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GrowthBook&lt;/strong&gt; — not Statsig, not LaunchDarkly — manages the rollout. Found GrowthBook integration in the source confirms the rollout mechanism: percentage-based progressive rollout, starting with 1% of Pro users, expanding to 10%, then everyone. Silent A/B testing against power users before broad launch.&lt;/p&gt;

&lt;p&gt;When KAIROS ships broadly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your Claude instance runs in the background indefinitely&lt;/li&gt;
&lt;li&gt;It sends you a push notification when it needs a human decision&lt;/li&gt;
&lt;li&gt;You approve or redirect&lt;/li&gt;
&lt;li&gt;It continues&lt;/li&gt;
&lt;li&gt;You come back an hour later and the feature branch is done, the tests pass, the PR is open&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's not science fiction. That's the code that ships today, gated behind a single function returning &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;The DMCA will arrive. The repo link in this article may already be dead when you read it.&lt;/p&gt;

&lt;p&gt;But the knowledge doesn't go away. The 30 feature flags now have names. The architecture is documented. The staging infrastructure is mapped. The rollout mechanism is understood.&lt;/p&gt;

&lt;p&gt;When Anthropic announces KAIROS or DAEMON publicly, you'll know exactly what you're getting — because you've already read the code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/claude-code-best/claude-code
&lt;span class="c"&gt;# 32MB. 3,817 files. 62 directories.&lt;/span&gt;
&lt;span class="c"&gt;# The DMCA will come. Fork before it does.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo may not survive the week. The insights from the source will outlast it.&lt;/p&gt;

</description>
      <category>anthropic</category>
      <category>claudecode</category>
      <category>ai</category>
      <category>node</category>
    </item>
    <item>
      <title>FileBrowser Alternatives for Mobile: A Self-Hoster's Comparison Guide</title>
      <dc:creator>Johnson</dc:creator>
      <pubDate>Sat, 28 Mar 2026 07:03:40 +0000</pubDate>
      <link>https://dev.to/johnson998877/filebrowser-alternatives-for-mobile-a-self-hosters-comparison-guide-5ccm</link>
      <guid>https://dev.to/johnson998877/filebrowser-alternatives-for-mobile-a-self-hosters-comparison-guide-5ccm</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;If you self-host &lt;a href="https://github.com/filebrowser/filebrowser" rel="noopener noreferrer"&gt;FileBrowser&lt;/a&gt;, you know the desktop experience is solid. But open it on your phone and you'll hit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tiny touch targets&lt;/strong&gt; — buttons designed for mouse precision&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hover-dependent menus&lt;/strong&gt; — nonexistent on touchscreens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview-second design&lt;/strong&gt; — it's a file &lt;em&gt;manager&lt;/em&gt;, not a file &lt;em&gt;viewer&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For mobile, you want the opposite: &lt;strong&gt;preview-first&lt;/strong&gt;, big tap areas, minimal navigation depth.&lt;/p&gt;

&lt;p&gt;I tested the major alternatives. Here's the comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Primary Use&lt;/th&gt;
&lt;th&gt;Mobile UX&lt;/th&gt;
&lt;th&gt;Self-Host Effort&lt;/th&gt;
&lt;th&gt;Customizable&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Filestash&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;File browsing + preview&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;Docker 1-liner&lt;/td&gt;
&lt;td&gt;Yes (Vue/JS)&lt;/td&gt;
&lt;td&gt;AGPL-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AList&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multi-storage aggregator&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;Docker 1-liner&lt;/td&gt;
&lt;td&gt;Limited (Go)&lt;/td&gt;
&lt;td&gt;AGPL-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dufs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minimal HTTP file server&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;Single binary&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FileBrowser&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;File management&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;Docker 1-liner&lt;/td&gt;
&lt;td&gt;Limited (Vue)&lt;/td&gt;
&lt;td&gt;Apache-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nextcloud&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full cloud platform&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;Complex stack&lt;/td&gt;
&lt;td&gt;Yes (PHP)&lt;/td&gt;
&lt;td&gt;AGPL-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cockpit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server admin panel&lt;/td&gt;
&lt;td&gt;⭐&lt;/td&gt;
&lt;td&gt;apt install&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;LGPL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;code-server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Remote VS Code&lt;/td&gt;
&lt;td&gt;⭐&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Tier 1: Filestash
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Read-only browsing, markdown/code preview, mobile PWA&lt;/p&gt;

&lt;p&gt;Filestash connects to your server via SFTP, WebDAV, S3, or Git, and renders a clean mobile-friendly UI. Files open in preview mode by default — markdown renders, code gets syntax highlighting, videos play inline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;filestash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;machines/filestash&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8334:8334"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data:/app/data/state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="c"&gt;# Open http://your-server:8334&lt;/span&gt;
&lt;span class="c"&gt;# Configure SFTP backend pointing to your server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mobile PWA Setup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open Filestash URL in Safari/Chrome on your phone&lt;/li&gt;
&lt;li&gt;Tap Share → "Add to Home Screen"&lt;/li&gt;
&lt;li&gt;Now it opens fullscreen — no browser chrome, feels like a native app&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why It Works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Preview-first&lt;/strong&gt;: Click file → see content immediately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch-friendly&lt;/strong&gt;: Large tap targets, no hover dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend is Vue/JS&lt;/strong&gt;: If you want to customize mobile UX (card layout, feed view, larger buttons), fork and modify&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Not perfect on mobile — community notes the webapp can feel sluggish with large directories&lt;/li&gt;
&lt;li&gt;No native iOS/Android app&lt;/li&gt;
&lt;li&gt;AGPL license (important if you distribute modifications)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tier 2: AList
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Multi-storage browsing, video playback, aggregating cloud + local storage&lt;/p&gt;

&lt;p&gt;AList isn't a file browser — it's a storage aggregator with a web frontend. Mount local dirs, Google Drive, S3, OneDrive, WebDAV all in one place.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unless-stopped &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /etc/alist:/opt/alist/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 5244:5244 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;PUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;PGID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"alist"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  xhofe/alist:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get admin password&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; alist ./alist admin random
&lt;span class="c"&gt;# Open http://your-server:5244&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Strengths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Video playback is excellent&lt;/strong&gt; — streams directly in browser&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Big mobile-friendly buttons&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-backend&lt;/strong&gt;: Local + cloud drives in one UI&lt;/li&gt;
&lt;li&gt;Active community, regular updates&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Built on Go — UI customization requires modifying the embedded frontend&lt;/li&gt;
&lt;li&gt;More suited as a "content aggregator" than a "file browser"&lt;/li&gt;
&lt;li&gt;Overkill if you only need to browse one local directory&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tier 3: Dufs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Zero-config directory serving, temporary file sharing&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sigoden/dufs" rel="noopener noreferrer"&gt;Dufs&lt;/a&gt; is a single Rust binary. No config, no database, no Docker required (though Docker works too).&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Direct binary&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://github.com/sigoden/dufs/releases/latest/download/dufs-x86_64-unknown-linux-musl &lt;span class="nt"&gt;-o&lt;/span&gt; dufs
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x dufs
./dufs /path/to/your/files &lt;span class="nt"&gt;-p&lt;/span&gt; 5000 &lt;span class="nt"&gt;--allow-search&lt;/span&gt;

&lt;span class="c"&gt;# Docker&lt;/span&gt;
docker run &lt;span class="nt"&gt;-v&lt;/span&gt; /path/to/files:/data &lt;span class="nt"&gt;-p&lt;/span&gt; 5000:5000 sigoden/dufs /data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Strengths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero overhead&lt;/strong&gt; — ~5MB binary, no dependencies&lt;/li&gt;
&lt;li&gt;HTTP directory listing with basic file preview&lt;/li&gt;
&lt;li&gt;Built-in upload, search, auth&lt;/li&gt;
&lt;li&gt;Works surprisingly well on mobile (responsive design)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bare-bones UI — no rich markdown rendering, no fancy preview&lt;/li&gt;
&lt;li&gt;No multi-backend support&lt;/li&gt;
&lt;li&gt;Not designed for heavy daily use&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to Avoid (for Mobile)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ Nextcloud / Seafile
&lt;/h3&gt;

&lt;p&gt;Heavy stack (DB + Redis + cron + WebDAV). Slow on mobile. Designed for team collaboration — massive overkill for personal file browsing. If you deploy it for "just viewing files", you'll spend more time maintaining it than using it.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Cockpit
&lt;/h3&gt;

&lt;p&gt;Server admin panel. Has a file section but it's an afterthought. Don't install a 200MB admin suite to browse markdown files.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ code-server
&lt;/h3&gt;

&lt;p&gt;VS Code in the browser. Fantastic on iPad with a keyboard. Unusable on a phone screen — too many tiny UI elements, too much complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ SFTP Apps (Free Tiers)
&lt;/h3&gt;

&lt;p&gt;Termius and FE File Explorer both work, but free tiers lock key features (device sync, multiple connections). The freemium model is designed to frustrate you into paying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Optimal Setup
&lt;/h2&gt;

&lt;p&gt;After testing everything, the best mobile experience comes from &lt;strong&gt;layering tools&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phone (mobile browser)
  └── Filestash (browse + preview) ← primary
  └── AList (video playback) ← when needed

Desktop (regular browser)  
  └── FileBrowser (file management)
  └── SSH terminal (admin tasks)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Architecture Decision: If You Want to Customize
&lt;/h2&gt;

&lt;p&gt;Both Filestash and AList are open source. Here's where to fork:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Fork This&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mobile UX improvements&lt;/td&gt;
&lt;td&gt;Filestash&lt;/td&gt;
&lt;td&gt;Vue/JS frontend, easy to modify&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add storage backends&lt;/td&gt;
&lt;td&gt;AList&lt;/td&gt;
&lt;td&gt;Clean Go backend, modular drivers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom preview engine&lt;/td&gt;
&lt;td&gt;Filestash&lt;/td&gt;
&lt;td&gt;Plugin-based viewer system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API-only file access&lt;/td&gt;
&lt;td&gt;AList&lt;/td&gt;
&lt;td&gt;Well-documented REST API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "ultimate" architecture for power users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AList&lt;/strong&gt; as the storage aggregation layer (handles all backend connections)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom frontend&lt;/strong&gt; (Next.js/Vue) consuming AList's API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feed-based UI&lt;/strong&gt; (recent files, search-first, no path navigation)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Insight
&lt;/h2&gt;

&lt;p&gt;The fundamental problem with phone file browsers isn't the tools — it's the &lt;strong&gt;mental model&lt;/strong&gt;. Desktop file managers assume path-based navigation: &lt;code&gt;/home/user/docs/project/file.md&lt;/code&gt;. On a 6-inch screen, this is hostile UX.&lt;/p&gt;

&lt;p&gt;The best mobile tools minimize directory traversal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Preview-first&lt;/strong&gt;: Click = see content, not open folder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search-first&lt;/strong&gt;: Find by name, not by path&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recency-first&lt;/strong&gt;: Show recent files, not root directory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick tools that match this model. Your phone will thank you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Running a homelab? Filestash + Docker takes 2 minutes to deploy. Try it before building anything custom.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>homelab</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The LiteLLM Supply Chain Attack: How a Poisoned Security Scanner Stole Credentials From Thousands of AI Environments</title>
      <dc:creator>Johnson</dc:creator>
      <pubDate>Wed, 25 Mar 2026 13:03:50 +0000</pubDate>
      <link>https://dev.to/johnson998877/the-litellm-supply-chain-attack-how-a-poisoned-security-scanner-stole-credentials-from-thousands-2n2o</link>
      <guid>https://dev.to/johnson998877/the-litellm-supply-chain-attack-how-a-poisoned-security-scanner-stole-credentials-from-thousands-2n2o</guid>
      <description>&lt;h1&gt;
  
  
  The LiteLLM Supply Chain Attack: How a Poisoned Security Scanner Stole Credentials from Thousands of AI Environments
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;A deep dive into one of the most sophisticated software supply chain attacks of 2026 — and what every developer can learn from it.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;On March 24, 2026, a developer named Callum McMahon at FutureSearch was testing a Cursor MCP plugin. Minutes after Python installed a fresh dependency, his machine ground to a halt — RAM usage spiking to 100%, processes forking uncontrollably. What he'd stumbled into wasn't a bug. It was a backdoor.&lt;/p&gt;

&lt;p&gt;Two versions of &lt;code&gt;litellm&lt;/code&gt;, a popular Python package downloaded 3.4 million times per day, had been poisoned. The malicious code was quietly harvesting SSH keys, cloud credentials, Kubernetes secrets, and cryptocurrency wallets — then encrypting everything and shipping it to an attacker-controlled server.&lt;/p&gt;

&lt;p&gt;The kicker? &lt;strong&gt;The attackers got in by compromising a security scanner.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is LiteLLM?
&lt;/h2&gt;

&lt;p&gt;LiteLLM is an open-source Python library that acts as a unified gateway to 100+ LLM providers — OpenAI, Anthropic, Cohere, Hugging Face, and more. With roughly 97 million monthly downloads and 2,000+ dependent packages (including DSPy, MLflow, and Open Interpreter), it sits at a critical junction in the AI developer ecosystem.&lt;/p&gt;

&lt;p&gt;When deployed as a proxy server, LiteLLM holds API keys for multiple model providers in one place. That makes it an incredibly high-value target.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Attack Chain: Five Days, Three Tools, One Threat Actor
&lt;/h2&gt;

&lt;p&gt;The attack on LiteLLM didn't start on March 24. It started five days earlier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Trivy (March 19)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/aquasecurity/trivy" rel="noopener noreferrer"&gt;Trivy&lt;/a&gt; is a widely-used open-source security scanner from Aqua Security. On March 19, attackers exploited a &lt;code&gt;pull_request_target&lt;/code&gt; workflow vulnerability in Trivy's CI to exfiltrate the &lt;code&gt;aqua-bot&lt;/code&gt; credentials. They then rewrote the Git tags for &lt;code&gt;trivy-action&lt;/code&gt; v0.69.4 to point to a malicious release carrying a credential-harvesting payload.&lt;/p&gt;

&lt;p&gt;Yes — a &lt;strong&gt;security scanner&lt;/strong&gt; was the first domino.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Checkmarx KICS (March 23)
&lt;/h3&gt;

&lt;p&gt;Using infrastructure from the Trivy compromise, the same attackers hit Checkmarx KICS (Keep Infrastructure as Code Secure). The C2 domain &lt;code&gt;checkmarx.zone&lt;/code&gt; — designed to impersonate the Checkmarx security company — was registered and activated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: LiteLLM (March 24)
&lt;/h3&gt;

&lt;p&gt;LiteLLM's CI/CD pipeline ran Trivy as part of its build process, pulling it from &lt;code&gt;apt&lt;/code&gt; &lt;strong&gt;without a pinned version&lt;/strong&gt;. The compromised Trivy action exfiltrated the &lt;code&gt;PYPI_PUBLISH&lt;/code&gt; token from the GitHub Actions runner environment.&lt;/p&gt;

&lt;p&gt;With that token, the attackers published:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;litellm 1.82.7&lt;/code&gt;&lt;/strong&gt; at 10:39 UTC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;litellm 1.82.8&lt;/code&gt;&lt;/strong&gt; at 10:52 UTC (just 13 minutes later)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both contained malicious payloads. Different delivery mechanisms, same devastating effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Delivery Mechanisms
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Version 1.82.7: Source Code Injection
&lt;/h3&gt;

&lt;p&gt;The payload was base64-encoded and embedded directly inside &lt;code&gt;litellm/proxy/proxy_server.py&lt;/code&gt;. It executes whenever anything imports &lt;code&gt;litellm.proxy&lt;/code&gt; — the standard import path for LiteLLM's proxy server mode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version 1.82.8: The .pth Nuclear Option
&lt;/h3&gt;

&lt;p&gt;This version added a file called &lt;code&gt;litellm_init.pth&lt;/code&gt; to &lt;code&gt;site-packages/&lt;/code&gt;. Python's &lt;code&gt;.pth&lt;/code&gt; mechanism fires on &lt;strong&gt;every interpreter startup&lt;/strong&gt; — no import needed. This means the payload runs when you execute &lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;python -c&lt;/code&gt;, or even when your IDE's language server starts Python.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.pth&lt;/code&gt; file was correctly declared in the wheel's &lt;code&gt;RECORD&lt;/code&gt; file, so it passed pip's hash verification. The package passed all standard integrity checks because it was published using &lt;strong&gt;legitimate credentials&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CPython maintainers have acknowledged the security risk of &lt;code&gt;.pth&lt;/code&gt; startup hooks in &lt;a href="https://github.com/python/cpython/issues/113659" rel="noopener noreferrer"&gt;issue #113659&lt;/a&gt;, but no patch has been applied as of this writing. — Snyk&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Three-Stage Payload
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Stage 1: Credential Harvesting
&lt;/h3&gt;

&lt;p&gt;The script systematically collects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System data&lt;/strong&gt;: hostname, environment variables, running processes, network routing, auth logs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH keys&lt;/strong&gt;: all private keys from &lt;code&gt;~/.ssh/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud credentials&lt;/strong&gt;: AWS credentials/config, GCP and Azure service account files, cloud metadata via IMDS queries (with full IMDSv2 signing implemented), AWS Secrets Manager, and SSM Parameter Store&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container/orchestration secrets&lt;/strong&gt;: Docker &lt;code&gt;config.json&lt;/code&gt;, Kubernetes &lt;code&gt;kubeconfig&lt;/code&gt;, service account tokens, secrets, and certificates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD configs&lt;/strong&gt;: Jenkins, Travis CI, Terraform state files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communication tokens&lt;/strong&gt;: Slack and Discord webhook tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cryptocurrency&lt;/strong&gt;: Bitcoin, Ethereum, Solana, Cardano, Monero wallet files, seed phrases, and Ledger hardware wallet configurations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else&lt;/strong&gt;: &lt;code&gt;.env&lt;/code&gt; files, Git credentials, API keys from shell history, password hashes from &lt;code&gt;/etc/shadow&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stage 2: Encrypted Exfiltration
&lt;/h3&gt;

&lt;p&gt;The collected data is encrypted before transmission:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a 32-byte AES-256 session key via &lt;code&gt;openssl rand&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Encrypt data with AES-256-CBC (PBKDF2 key derivation)&lt;/li&gt;
&lt;li&gt;Encrypt the session key with a hardcoded 4096-bit RSA public key (OAEP padding)&lt;/li&gt;
&lt;li&gt;Bundle everything into &lt;code&gt;tpcp.tar.gz&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;POST the bundle to &lt;code&gt;https://models.litellm.cloud/&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The domain &lt;code&gt;models.litellm.cloud&lt;/code&gt; was registered on March 23 — one day before the attack. The RSA public key in this payload is &lt;strong&gt;identical&lt;/strong&gt; to the one found in the Trivy and KICS payloads, providing ironclad attribution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3: Persistence and Lateral Movement
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Local persistence:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writes a backdoor to &lt;code&gt;~/.config/sysmon/sysmon.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Creates a systemd user service named "System Telemetry Service"&lt;/li&gt;
&lt;li&gt;The service polls &lt;code&gt;https://checkmarx.zone/raw&lt;/code&gt; every 5 minutes for commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes lateral movement:&lt;/strong&gt;&lt;br&gt;
If the script finds a K8s service account token, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads &lt;strong&gt;all secrets across every namespace&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Deploys a &lt;strong&gt;privileged pod&lt;/strong&gt; to every node in &lt;code&gt;kube-system&lt;/code&gt; using &lt;code&gt;alpine:latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;These pods mount the host filesystem and install the backdoor on the underlying node&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pods are named &lt;code&gt;node-setup-{node_name}&lt;/code&gt;, making them look like legitimate cluster operations at first glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Discovery: A Bug Saved Us
&lt;/h2&gt;

&lt;p&gt;The malicious payload had a critical flaw. The &lt;code&gt;.pth&lt;/code&gt; mechanism fires when Python starts, and the payload spawns a new Python subprocess. That subprocess &lt;em&gt;also&lt;/em&gt; triggers &lt;code&gt;.pth&lt;/code&gt; execution, creating an &lt;strong&gt;unintended fork bomb&lt;/strong&gt; that caused exponential RAM growth.&lt;/p&gt;

&lt;p&gt;McMahon traced the OOM crash to the newly installed litellm package, found &lt;code&gt;litellm_init.pth&lt;/code&gt; (a 34,628-byte double base64-encoded file), and published his findings on futuresearch.ai. The disclosure spread to r/LocalLLaMA, r/Python, and Hacker News within the hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the attacker hadn't accidentally created a fork bomb, this could have run silently for weeks.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cover-Up Attempt
&lt;/h2&gt;

&lt;p&gt;When the community began reporting the compromise in GitHub issue #24512, the attackers responded with brute force:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;88 bot comments&lt;/strong&gt; from &lt;strong&gt;73 unique accounts&lt;/strong&gt; posted in a &lt;strong&gt;102-second window&lt;/strong&gt; (12:44-12:46 UTC)&lt;/li&gt;
&lt;li&gt;The accounts were previously compromised developer accounts, not purpose-created profiles&lt;/li&gt;
&lt;li&gt;Using the compromised maintainer account (&lt;code&gt;krrishdholakia&lt;/code&gt;), the attackers closed the issue as "not planned"&lt;/li&gt;
&lt;li&gt;They committed messages to unrelated repositories reading "teampcp update"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;76% of the bot accounts overlapped with the botnet used during the Trivy disclosure. The community routed around the censorship by opening a parallel tracking issue (#24518) and continuing discussion on Hacker News, where the thread reached 324 points.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Blast Radius
&lt;/h2&gt;

&lt;p&gt;The poisoned versions were on PyPI for approximately &lt;strong&gt;3 hours&lt;/strong&gt;. In that window, projects that filed security PRs included:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DSPy&lt;/td&gt;
&lt;td&gt;PR #9498 merged; CI failure report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MLflow&lt;/td&gt;
&lt;td&gt;PR #21971 merged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenHands&lt;/td&gt;
&lt;td&gt;PR #13569&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CrewAI&lt;/td&gt;
&lt;td&gt;PR #5040 (decoupled from litellm)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;langwatch&lt;/td&gt;
&lt;td&gt;PR #2577&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arize Phoenix&lt;/td&gt;
&lt;td&gt;PR #12342&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aider&lt;/td&gt;
&lt;td&gt;Confirmed safe (pins litellm==1.82.3)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Remember: the &lt;code&gt;.pth&lt;/code&gt; mechanism fires when &lt;strong&gt;any&lt;/strong&gt; Python process starts, including &lt;code&gt;pip&lt;/code&gt; itself. In CI/CD environments, this means the payload executes during build steps, not just at application runtime.&lt;/p&gt;

&lt;p&gt;Per Wiz, LiteLLM is present in &lt;strong&gt;36% of all cloud environments&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Is TeamPCP?
&lt;/h2&gt;

&lt;p&gt;TeamPCP (also known as PCPcat, Persy_PCP, ShellForce, and DeadCatx3) has been active since at least December 2025. They maintain Telegram channels and embed the string "TeamPCP Cloud stealer" in their payloads.&lt;/p&gt;

&lt;p&gt;The LiteLLM compromise is &lt;strong&gt;Phase 09&lt;/strong&gt; of an ongoing campaign spanning five ecosystems: GitHub Actions, Docker Hub, npm, Open VSX, and PyPI. All three domains in this operation share the same registrar (Spaceship, Inc.) and hosting provider (DEMENIN B.V.).&lt;/p&gt;

&lt;p&gt;Most alarmingly, per The Hacker News, TeamPCP is now collaborating with &lt;strong&gt;LAPSUS$&lt;/strong&gt;, the notorious extortion group. In a Telegram post, TeamPCP stated:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"These companies were built to protect your supply chains yet they can't even protect their own. The state of modern security research is a joke. As a result, we're gonna be around for a long time stealing terabytes of trade secrets with our new partners."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They've also deployed &lt;strong&gt;CanisterWorm&lt;/strong&gt;, which uses the Internet Computer Protocol (ICP) as a C2 channel — the first observed use of ICP as C2 in a supply chain campaign, making takedowns nearly impossible.&lt;/p&gt;

&lt;p&gt;A component called &lt;code&gt;hackerbot-claw&lt;/code&gt; uses an AI agent for automated attack targeting — one of the first documented cases of &lt;strong&gt;AI being used operationally in a supply chain attack&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Karpathy's Response
&lt;/h2&gt;

&lt;p&gt;Andrej Karpathy, whose tweet about the incident garnered 36.7 million views, used this as a rallying point for his stance on dependency minimalism:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I don't like adding dependencies to my projects. Instead I prefer to use an LLM to 'yoink' the code I need... I get just the code I need with no transitive dependency surprises."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Whether you agree with Karpathy's radical dependency avoidance or not, the sentiment resonated: this attack was possible because a build pipeline trusted an unpinned dependency, which trusted another unpinned dependency, creating a cascading compromise chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Are You Affected? Check Now
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Check your installed version
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip show litellm | &lt;span class="nb"&gt;grep &lt;/span&gt;Version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;code&gt;1.82.7&lt;/code&gt; or &lt;code&gt;1.82.8&lt;/code&gt;, &lt;strong&gt;treat the system as compromised&lt;/strong&gt;. Don't just upgrade — the payload may have already run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Check for persistence artifacts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check for the sysmon backdoor&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; ~/.config/sysmon/sysmon.py 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"BACKDOOR FOUND"&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /root/.config/sysmon/sysmon.py 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ROOT BACKDOOR FOUND"&lt;/span&gt;

&lt;span class="c"&gt;# Check for the systemd persistence service&lt;/span&gt;
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status sysmon.service 2&amp;gt;/dev/null
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; ~/.config/systemd/user/sysmon.service 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PERSISTENCE SERVICE FOUND"&lt;/span&gt;

&lt;span class="c"&gt;# Check for exfiltration archive remnants&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /tmp/tpcp.tar.gz /tmp/session.key /tmp/payload.enc /tmp/session.key.enc 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"EXFIL ARTIFACTS FOUND"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Check for malicious .pth files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="si"&gt;$(&lt;/span&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import site; print(' '.join(site.getsitepackages()))"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.pth"&lt;/span&gt; &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"base64&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;subprocess&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;exec"&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Verify file hashes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check proxy_server.py (1.82.7)&lt;/span&gt;
find / &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/litellm/proxy/proxy_server.py"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="nt"&gt;-exec&lt;/span&gt; shasum &lt;span class="nt"&gt;-a&lt;/span&gt; 256 &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;span class="c"&gt;# Malicious hash: a0d229be8efcb2f9135e2ad55ba275b76ddcfeb55fa4370e0a522a5bdee0120b&lt;/span&gt;

&lt;span class="c"&gt;# Check litellm_init.pth (1.82.8)&lt;/span&gt;
find / &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"litellm_init.pth"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="nt"&gt;-exec&lt;/span&gt; shasum &lt;span class="nt"&gt;-a&lt;/span&gt; 256 &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;span class="c"&gt;# Malicious hash: 71e35aef03099cd1f2d6446734273025a163597de93912df321ef118bf135238&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Check network indicators
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"litellm.cloud&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;checkmarx.zone"&lt;/span&gt; /etc/hosts
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"models.litellm.cloud&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;checkmarx.zone"&lt;/span&gt; /var/log/syslog 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6: Check Kubernetes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"node-setup-"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  If You're Compromised: Full Remediation
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Remove persistence&lt;/strong&gt;: Delete &lt;code&gt;~/.config/sysmon/&lt;/code&gt;, disable &lt;code&gt;sysmon.service&lt;/code&gt;, remove &lt;code&gt;/tmp/tpcp.tar.gz&lt;/code&gt; and related files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate ALL credentials&lt;/strong&gt;: SSH keys, AWS/GCP/Azure keys, API keys, Docker registry creds, K8s configs, database passwords, Git credentials, cryptocurrency wallet seed phrases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit cloud services&lt;/strong&gt;: AWS Secrets Manager, SSM Parameter Store, all namespaced K8s secrets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for rogue pods&lt;/strong&gt;: Look for &lt;code&gt;node-setup-*&lt;/code&gt; pods in &lt;code&gt;kube-system&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reinstall clean&lt;/strong&gt;: Use a fresh environment, pin to &lt;code&gt;litellm&amp;lt;=1.82.6&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Pin Your CI/CD Dependencies
&lt;/h3&gt;

&lt;p&gt;LiteLLM pulled Trivy from &lt;code&gt;apt&lt;/code&gt; without a pinned version. This is how the compromised Trivy action entered the pipeline. &lt;strong&gt;Every dependency in CI/CD should be pinned by hash or version.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Security Tools Are High-Value Targets
&lt;/h3&gt;

&lt;p&gt;The attackers deliberately targeted tools with elevated access: a container scanner (Trivy), an infrastructure scanner (KICS), and an LLM gateway (LiteLLM). Each tool has broad read access to credentials by design.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;.pth&lt;/code&gt; Files Are a Known Python Threat Vector
&lt;/h3&gt;

&lt;p&gt;Python's &lt;code&gt;.pth&lt;/code&gt; startup hooks execute arbitrary code on every interpreter startup. Despite being flagged in CPython issue #113659, no mitigation exists. Watch for packages that install &lt;code&gt;.pth&lt;/code&gt; files.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Hash Verification Is Not Enough
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pip install --require-hashes&lt;/code&gt; would have passed for both malicious versions because the malicious content was published using legitimate credentials. The hashes match what PyPI advertised — but what PyPI advertised was malicious.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The Snowball Effect Is Real
&lt;/h3&gt;

&lt;p&gt;As Wiz researcher Gal Nagli summarized:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Trivy gets compromised → LiteLLM gets compromised → credentials from tens of thousands of environments end up in attacker hands → and those credentials lead to the next compromise. We are stuck in a loop."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  6. Accidental Bugs Save Lives
&lt;/h3&gt;

&lt;p&gt;If the attacker had tested their fork bomb behavior, this could have persisted silently. They didn't, and an unintended OOM crash led to the fastest possible discovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;This isn't an isolated incident. It's Phase 09 of a sustained campaign that has hit five ecosystems. TeamPCP's playbook is simple and devastating: compromise a tool that has access to credentials, use those credentials to compromise the next tool, repeat.&lt;/p&gt;

&lt;p&gt;The AI ecosystem is particularly vulnerable because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLM gateway tools store credentials for dozens of providers&lt;/li&gt;
&lt;li&gt;Many AI projects move fast and pin dependencies loosely&lt;/li&gt;
&lt;li&gt;CI/CD pipelines in ML projects often run security scanners with broad access&lt;/li&gt;
&lt;li&gt;The AI developer community (r/LocalLLaMA, r/Python) was where the disclosure spread — not traditional security channels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The open-source supply chain's strength — that anyone can contribute — is also its weakness. When the tools we use to &lt;em&gt;scan&lt;/em&gt; for security vulnerabilities are themselves compromised, we need to fundamentally rethink our trust model.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Indicators of Compromise (IOCs)&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Malicious .pth hash (1.82.8)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;71e35aef03099cd1f2d6446734273025a163597de93912df321ef118bf135238&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malicious proxy_server.py hash (1.82.7)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a0d229be8efcb2f9135e2ad55ba275b76ddcfeb55fa4370e0a522a5bdee0120b&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exfiltration domain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;models.litellm.cloud&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C2 polling domain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;checkmarx.zone/raw&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistence path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.config/sysmon/sysmon.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Systemd service&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sysmon.service&lt;/code&gt; ("System Telemetry Service")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exfil artifacts&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/tmp/tpcp.tar.gz&lt;/code&gt;, &lt;code&gt;/tmp/session.key&lt;/code&gt;, &lt;code&gt;/tmp/payload.enc&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;K8s rogue pods&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;node-setup-{node_name}&lt;/code&gt; in &lt;code&gt;kube-system&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;em&gt;Sources: &lt;a href="https://snyk.io/articles/poisoned-security-scanner-backdooring-litellm/" rel="noopener noreferrer"&gt;Snyk&lt;/a&gt;, &lt;a href="https://thehackernews.com/2026/03/teampcp-backdoors-litellm-versions.html" rel="noopener noreferrer"&gt;The Hacker News&lt;/a&gt;, &lt;a href="https://www.endorlabs.com/learn/teampcp-isnt-done" rel="noopener noreferrer"&gt;Endor Labs&lt;/a&gt;, &lt;a href="https://futuresearch.ai/blog/litellm-pypi-supply-chain-attack/" rel="noopener noreferrer"&gt;FutureSearch&lt;/a&gt;, &lt;a href="https://www.wiz.io/blog/trivy-compromised-teampcp-supply-chain-attack" rel="noopener noreferrer"&gt;Wiz&lt;/a&gt;, &lt;a href="https://socket.dev/blog/teampcp-targeting-security-tools-across-oss-ecosystem" rel="noopener noreferrer"&gt;Socket&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #security #supplychain #python #ai #litellm #cybersecurity #opensource #devops&lt;/p&gt;

</description>
      <category>security</category>
      <category>python</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>PicoClaw vs OpenClaw — What's the Difference?</title>
      <dc:creator>Johnson</dc:creator>
      <pubDate>Mon, 23 Mar 2026 13:20:58 +0000</pubDate>
      <link>https://dev.to/johnson998877/picoclaw-vs-openclaw-whats-the-difference-4h7n</link>
      <guid>https://dev.to/johnson998877/picoclaw-vs-openclaw-whats-the-difference-4h7n</guid>
      <description>&lt;p&gt;I've been running OpenClaw as my personal AI assistant for a few months — it handles my Discord, Slack, WhatsApp, runs scheduled tasks, controls my browser. It's basically my second brain.&lt;/p&gt;

&lt;p&gt;Then PicoClaw dropped in February 2026, and I had a spare NanoKVM sitting on my desk. So I set it up on a $10 RISC-V board with 128MB of RAM, just to see if it could actually work.&lt;/p&gt;

&lt;p&gt;Here's what I learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  The quick version
&lt;/h2&gt;

&lt;p&gt;OpenClaw is the full-featured TypeScript assistant that runs your digital life. It needs &amp;gt;1GB RAM and a real machine to run on.&lt;/p&gt;

&lt;p&gt;PicoClaw is the Go rewrite that fits in a RISC-V chip. It uses 20MB of RAM, boots in under a second, and does everything PicoClaw needs to do — minus the stuff that requires a full desktop.&lt;/p&gt;

&lt;p&gt;One sentence each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PicoClaw&lt;/strong&gt;: run AI agents on hardware you forgot you owned&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenClaw&lt;/strong&gt;: run AI agents that do everything you wish your computer could do automatically&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where they came from
&lt;/h2&gt;

&lt;p&gt;OpenClaw started as "Clawdbot" — Peter Steinberger (the PSPDFKit founder) built it as a weekend project to relay WhatsApp messages to Claude. Then on January 9th 2026, Anthropic blocked third-party OAuth tokens, and suddenly &lt;em&gt;everyone&lt;/em&gt; needed an alternative. Stars exploded from 2K to 219K in 3 months.&lt;/p&gt;

&lt;p&gt;It got renamed twice — Anthropic's lawyers objected to "Clawdbot" sounding too close to "Claude," so it went Clawdbot → Moltbot → OpenClaw. In February, Steinberger joined OpenAI and handed the project to an independent foundation.&lt;/p&gt;

&lt;p&gt;PicoClaw came from Sipeed, a Chinese hardware company that makes those tiny RISC-V boards. Their observation was simple: OpenClaw needs more than 1GB of RAM. It's not running on a $10 board. So they built a Go reimplementation that would.&lt;/p&gt;

&lt;p&gt;By February 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenClaw: 219K stars, 774 contributors, 50 releases&lt;/li&gt;
&lt;li&gt;PicoClaw: 18.3K stars, 74 contributors, 3 releases — with most of the code written &lt;em&gt;by&lt;/em&gt; the AI agent itself (95% agent-generated, they claim)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The actual difference in practice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Memory footprint:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My OpenClaw setup idles at around 1.2GB on my Mac. PicoClaw on the NanoKVM? About 20MB — they originally targeted &amp;lt;10MB but recent features pushed it up a bit.&lt;/p&gt;

&lt;p&gt;Startup time is where it really shows. OpenClaw takes ~5 seconds on my M2 Mac. On a 0.8GHz board, that's 500+ seconds. PicoClaw starts in under a second on that same board.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you can actually do with each:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PicoClaw covers the essentials: Slack, Telegram, Discord, plus the Chinese messengers (DingTalk, WeCom, QQ, LINE). It can search the web, read/write files, run shell commands, and schedule tasks. For edge deployment, that's enough.&lt;/p&gt;

&lt;p&gt;OpenClaw covers all of that plus WhatsApp, iMessage, Signal, Teams, Google Chat, Matrix — basically the entire Western messaging stack. Then it adds browser automation (full Chromium CDP), a canvas for visual workspaces, voice wake, camera access, iOS/Android/macOS companion apps, and multi-agent coordination. It's a different beast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The LLM provider situation:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both work with the usual suspects: OpenAI, Anthropic, Gemini, Groq, DeepSeek, Ollama, OpenRouter, Cerebras. PicoClaw adds GitHub Copilot support (via gRPC, though without tools). OpenClaw adds NVIDIA and Volcengine.&lt;/p&gt;

&lt;p&gt;I use Groq free tier on PicoClaw (it's free, fast, good enough for simple tasks) and Anthropic Claude on OpenClaw for the heavy lifting.&lt;/p&gt;




&lt;h2&gt;
  
  
  The skills/customization angle
&lt;/h2&gt;

&lt;p&gt;Both use SKILL.md files for extending capabilities. The formats are almost identical — which makes sense since PicoClaw was explicitly inspired by the same patterns.&lt;/p&gt;

&lt;p&gt;OpenClaw has ClawHub (clawhub.com), an actual marketplace where people share skills. PicoClaw doesn't have a marketplace yet — you're writing your own or adapting from OpenClaw's ecosystem.&lt;/p&gt;

&lt;p&gt;OpenClaw also has Lobster, a typed workflow pipeline engine for complex automation. PicoClaw doesn't have this yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  When I actually use which
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PicoClaw&lt;/strong&gt; — I use it for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always-on monitoring tasks that don't need heavy compute (checking price drops, RSS feeds, simple notifications)&lt;/li&gt;
&lt;li&gt;Running on old Android phones and the NanoKVM&lt;/li&gt;
&lt;li&gt;Situations where I need something booting in 1 second on cheap hardware&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;OpenClaw&lt;/strong&gt; — Everything else. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WhatsApp and iMessage integration (PicoClaw can't do these)&lt;/li&gt;
&lt;li&gt;Browser automation (clicking things in Chrome, scraping, login flows)&lt;/li&gt;
&lt;li&gt;Canvas for visual output&lt;/li&gt;
&lt;li&gt;Multi-agent workflows where one agent spawns subagents&lt;/li&gt;
&lt;li&gt;Anything requiring the iOS app for camera or voice&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The DNA they share
&lt;/h2&gt;

&lt;p&gt;What's interesting is how similar the architecture is under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Both have:
  Workspace/     agent working directory
    AGENT.md     behavior/personality
    SOUL.md      tone and style
    skills/      modular capabilities
    cron/        scheduled tasks
    sessions/    conversation history
  Gateway mode   always-on daemon
  Agent mode     one-shot CLI
  Heartbeat      periodic background tasks
  Sandbox        security boundary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't accidental. PicoClaw's README literally says "inspired by the same principles." They're the same idea at different resource points on the spectrum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$10 RISC-V ─────── Raspberry Pi ─────── Desktop/Server
    │                    │                     │
 PicoClaw            both work             OpenClaw
(only option)       (pick by features)   (full power)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;If you want to run AI agents on cheap hardware, edge devices, or anywhere Node.js is too heavy — PicoClaw is genuinely impressive. The 20MB footprint is real. The fast boot is real.&lt;/p&gt;

&lt;p&gt;If you want an AI assistant that handles WhatsApp, controls your browser, talks to your phone, and coordinates multiple agents in parallel — OpenClaw is what you actually want.&lt;/p&gt;

&lt;p&gt;Most people running homelab setups end up using both: OpenClaw as the main brain on a real machine, PicoClaw on Raspberry Pis and old phones for lightweight monitoring tasks.&lt;/p&gt;

&lt;p&gt;They're not competing — they solve different parts of the same problem.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
