<?xml version="1.0" encoding="UTF-8"?>
<!-- generator="FeedCreator 1.8" -->
<rss version="2.0">
    <channel xmlns:g="http://base.google.com/ns/1.0">
        <title>Linkblog</title>
        <description></description>
        <link></link>
        <lastBuildDate>Mon, 22 Jun 2026 04:04:48 +0000</lastBuildDate>
        <generator>FeedCreator 1.8</generator>
        <item>
            <title>ANDREAS GOHR IS IN THE WEIGHTS</title>
            <link>https://intheweights.com/p/andreas-gohr</link>
            <description></description>
            <pubDate>Sun, 21 Jun 2026 05:34:36 +0000</pubDate>
            <guid>https://intheweights.com/p/andreas-gohr</guid>
        </item>
        <item>
            <title>Alternative Firmwares - ReadMe.club</title>
            <link>https://www.readme.club/firmware</link>
            <description>&lt;blockquote&gt;&lt;hr&gt;&lt;/blockquote&gt;&lt;div href=&quot;/how-to-flash&quot;&gt; &lt;p&gt;First time flashing?&lt;/p&gt; &lt;p&gt;
Read our guide on risks, backups, and installation. Then pick a firmware and make your device yours.
&lt;/p&gt; &lt;div&gt; &lt;p&gt;&lt;span&gt;Open the guide&lt;/span&gt;&lt;/p&gt;&lt;svg viewBox=&quot;0 0 20 20&quot; fill=&quot;currentColor&quot;&gt; &lt;path fill-rule=&quot;evenodd&quot; d=&quot;M10.293 3.293a1 1 0 011.414 0L16 7.586a1 1 0 010 1.414l-4.293 4.293a1 1 0 01-1.414-1.414L12.586 9H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z&quot; clip-rule=&quot;evenodd&quot; /&gt; &lt;/svg&gt; &lt;/div&gt; &lt;/div&gt;</description>
            <pubDate>Fri, 19 Jun 2026 06:10:02 +0000</pubDate>
            <guid>https://www.readme.club/firmware</guid>
        </item>
        <item>
            <title>SQL to ER Diagram — Free Online ERD Generator from SQL (no signup)</title>
            <link>https://sqltoerdiagram.com/</link>
            <description>&lt;blockquote&gt;&lt;hr&gt;&lt;/blockquote&gt;&lt;div&gt;
  &lt;h2&gt;SQL to ER Diagram — free online ERD generator: convert a SQL schema (CREATE TABLE statements) into an interactive entity-relationship diagram in your browser. Turn SQL into a diagram instantly, no signup.&lt;/h2&gt;

  &lt;header&gt;
    &lt;p&gt;
      &lt;span&gt;SQL&amp;nbsp;to&amp;nbsp;ER&amp;nbsp;Diagram&lt;/span&gt;
    &lt;/p&gt;

    
  &lt;/header&gt;

  &lt;main id=&quot;layout&quot;&gt;
    

    

    &lt;section&gt;
      &lt;canvas id=&quot;canvas&quot;&gt;&lt;/canvas&gt;

      

      &lt;div id=&quot;empty&quot;&gt;
          &lt;h2&gt;Paste SQL, see the schema.&lt;/h2&gt;
          &lt;p&gt;Drop your &lt;code&gt;CREATE TABLE&lt;/code&gt; statements on the left. Drag tables, scroll to zoom, double-click to rename, export when done.&lt;/p&gt;
          &lt;p&gt;
            
            100% local — your schema never leaves your browser. No accounts, no uploads.
          &lt;/p&gt;
        &lt;/div&gt;

      

      

      
    &lt;/section&gt;
  &lt;/main&gt;

  
  &lt;section aria-label=&quot;About SQL to ER Diagram&quot;&gt;
    
    &lt;p&gt;SQL to ER Diagram is a free, open-source tool that converts a SQL schema into an
      interactive entity-relationship diagram (ERD) right in your browser. Paste your
      &lt;code&gt;CREATE TABLE&lt;/code&gt; statements and instantly visualize tables, columns, primary
      keys, foreign keys and relationships. Works with PostgreSQL, MySQL, SQLite and SQL
      Server. Drag tables, auto-arrange the layout, add notes, and export to PNG or SVG.
      Nothing is uploaded — your schema stays on your machine. It also supports
      &lt;a href=&quot;/prisma/&quot;&gt;Prisma schemas&lt;/a&gt;, SQLAlchemy and Sequelize models.&lt;/p&gt;
    &lt;h2&gt;Frequently asked questions&lt;/h2&gt;
    &lt;h3&gt;How do I create an ER diagram from SQL?&lt;/h3&gt;
    &lt;p&gt;Paste your SQL &lt;code&gt;CREATE TABLE&lt;/code&gt; statements into the editor and SQL to ER
      Diagram instantly renders an interactive entity-relationship diagram. Drag tables to
      arrange them, then export as PNG or SVG.&lt;/p&gt;
    &lt;h3&gt;Which SQL dialects are supported?&lt;/h3&gt;
    &lt;p&gt;It parses standard &lt;code&gt;CREATE TABLE&lt;/code&gt; and &lt;code&gt;ALTER TABLE&lt;/code&gt; DDL and works
      with PostgreSQL, MySQL, SQLite and SQL Server syntax, including primary keys, foreign
      keys, unique and not-null constraints.&lt;/p&gt;
    &lt;h3&gt;Is it free?&lt;/h3&gt;
    &lt;p&gt;Yes. SQL to ER Diagram is completely free and open source, with no account or sign-up
      required.&lt;/p&gt;
    &lt;h3&gt;Is my data private? Does my SQL get uploaded?&lt;/h3&gt;
    &lt;p&gt;Everything runs locally in your browser. Your SQL schema is never uploaded to or stored
      on any server.&lt;/p&gt;
    &lt;h3&gt;Can I export the diagram?&lt;/h3&gt;
    &lt;p&gt;Yes. You can export a high-resolution PNG or a vector SVG, save the full project as a
      file, or copy a shareable link that encodes the diagram in the URL.&lt;/p&gt;
    &lt;h3&gt;Do I need to install anything?&lt;/h3&gt;
    &lt;p&gt;No installation needed. It runs entirely in your web browser on both desktop and mobile.&lt;/p&gt;
  &lt;/section&gt;

&lt;/div&gt;</description>
            <pubDate>Sun, 14 Jun 2026 06:10:47 +0000</pubDate>
            <guid>https://sqltoerdiagram.com/</guid>
        </item>
        <item>
            <title>Vibe Engineering a Personal Tool</title>
            <link>https://www.cosmocode.de/en/blog/agoh/20260609-vibe-engineering/</link>
            <description>
&lt;p&gt;There are two approaches when it comes to using modern coding agents. On one end of the spectrum is pure vibe coding: you let the agent build and never look at the code. The other end has been dubbed agentic engineering: you treat the agent as a co-developer, and you discuss and review all changes in detail.&lt;/p&gt;
&lt;p&gt;At CosmoCode, we usually use the latter approach. We want to own and understand the code we deliver. &lt;strong&gt;Coding agents can help us deliver better code, not necessarily more code or code built faster&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But if the approach to coding agents is a spectrum, then there should be a middle ground as well. I like to call it “vibe engineering”. Vibe engineering is a more relaxed approach to coding agents: you still review the code it generates, but you don’t scrutinize every line of it.&lt;/p&gt;
&lt;p&gt;I like this approach for personal tools that aren’t purely one-off scripts, but also aren’t production code. These are tools that I want to use for myself, but that I don’t necessarily want to turn into real “projects”.&lt;/p&gt;
&lt;p&gt;One of my colleagues asked me if I could describe how I approach &lt;strong&gt;vibe engineering&lt;/strong&gt; for such &lt;strong&gt;a personal tool&lt;/strong&gt;, using a real example. This is easier said than done, but I’ll try anyway. I will give some examples of the kind of interactions I have with the agent without reproducing the entire conversation. I’ll hide the full Claude answers behind a &lt;code&gt;details&lt;/code&gt; tag, so you can drill into them if you want to see the full context. I will still omit the tool calls.&lt;/p&gt;
&lt;h2&gt;Step 1: Brainstorming&lt;/h2&gt;
&lt;p&gt;We’re running most of our services in a &lt;strong&gt;Kubernetes cluster&lt;/strong&gt;, and sometimes you need to move files to or from a storage volume mounted to a specific pod. This can be done with &lt;code&gt;kubectl cp&lt;/code&gt;. However, &lt;code&gt;kubectl cp&lt;/code&gt; does not take care of copying last modified dates. The workaround is to use &lt;code&gt;kubectl exec&lt;/code&gt; and tar to stream to stdout, then use a second tar to read stdin on the other side: classic Unix tooling. But constructing the right &lt;code&gt;kubectl exec&lt;/code&gt; call is non-trivial.&lt;/p&gt;
&lt;p&gt;I wanted a simple, Midnight Commander-like, &lt;strong&gt;two-panel TUI interface for copying files&lt;/strong&gt;. One side shows my local computer and the pod’s filesystem is shown on the other side. Files can simply be selected and copied by the press of a button.&lt;/p&gt;
&lt;p&gt;To figure out how feasible this is, I actually started in Claude chat. I find that the chat interface is a better way to do initial brainstorming than jumping right into Claude Code - especially when I am not even sure I want to pursue the project in the end. I can ask questions, get suggestions, and iterate on the idea without constantly having to tell Claude not to start coding. But yes, I could also have used Claude Code’s plan mode.&lt;/p&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;question: how complicated would it be to create a simple TUI tool that uses kubectl and tar to allow for easy file transfer between the local machine and a kubernetes pod using a two panel layout? (tar to preserve timestamps and file permissions)&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude explains how it could work and offers three approaches&lt;/summary&gt;&lt;div&gt;&lt;p&gt;Not very complicated. A working version is a weekend project; a polished one is maybe a week.&lt;/p&gt;&lt;p&gt;Here’s the breakdown:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The core mechanics are easy.&lt;/strong&gt; kubectl already does the heavy lifting:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;List remote files: &lt;code&gt;kubectl exec &amp;lt;pod&amp;gt; -- ls -la --time-style=full-iso /path&lt;/code&gt; (parse the output)&lt;/li&gt;
&lt;li&gt;Pull a file/dir preserving metadata: &lt;code&gt;kubectl exec &amp;lt;pod&amp;gt; -- tar cf - -C /parent target | tar xf - -C /local/dest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Push: &lt;code&gt;tar cf - -C /local target | kubectl exec -i &amp;lt;pod&amp;gt; -- tar xf - -C /remote/dest&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;This is essentially what &lt;code&gt;kubectl cp&lt;/code&gt; does internally (it’s tar-based), but you get to control flags like &lt;code&gt;--numeric-owner&lt;/code&gt;, &lt;code&gt;-p&lt;/code&gt;, etc. yourself.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The TUI is the bulk of the work.&lt;/strong&gt; A two-panel layout (local on left, remote on right, Tab to switch focus, arrows to navigate, Enter to descend, F5/F6 to copy — basically Midnight Commander) is a well-trodden pattern. The real effort goes into directory navigation state, scrolling, selection, and async transfers so the UI doesn’t freeze.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Recommended stack:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Go&lt;/strong&gt; — &lt;code&gt;bubbletea&lt;/code&gt;/&lt;code&gt;bubbles&lt;/code&gt; for the TUI, and you can use &lt;code&gt;client-go&lt;/code&gt; to talk to the API directly (including the exec/SPDY streaming) instead of shelling out. Natural fit since kubectl is Go.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python&lt;/strong&gt; — &lt;code&gt;textual&lt;/code&gt; for the TUI is very fast to build in; shell out to &lt;code&gt;kubectl&lt;/code&gt; or use the &lt;code&gt;kubernetes&lt;/code&gt; client library. Lowest effort to a prototype.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rust&lt;/strong&gt; — &lt;code&gt;ratatui&lt;/code&gt; if you want a single static binary and don’t mind more work.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;The fiddly parts&lt;/strong&gt; (where the time actually goes):&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Pods often lack &lt;code&gt;tar&lt;/code&gt; (especially distroless/scratch images) — you need a fallback or a clear error.&lt;/li&gt;
&lt;li&gt;Multi-container pods need &lt;code&gt;-c &amp;lt;container&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Async progress reporting for large transfers without blocking the event loop.&lt;/li&gt;
&lt;li&gt;Error handling for permissions, broken pipes, missing paths.&lt;/li&gt;
&lt;li&gt;Context/namespace selection.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;If you shell out to &lt;code&gt;kubectl&lt;/code&gt; and use Textual or Bubbletea, a usable single-pod-context prototype is genuinely a 1–2 day effort. Going to API-direct streaming, progress bars, multi-namespace browsing, and the tar-missing fallbacks is what stretches it toward a week.&lt;/p&gt;&lt;p&gt;Want me to sketch out a starter implementation in one of these stacks?&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;Hmm I would tend towards go because I like the single binary and I know a bit of go. But you say it’s the most difficult option?&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude suggests to keep things simple&lt;/summary&gt;&lt;div&gt;&lt;p&gt;Not the most difficult — I’d put it in the middle. The ordering I had in mind:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python/Textual&lt;/strong&gt; — fastest to a prototype, but you don’t get the single binary you want (you’re shipping a Python env or doing PyInstaller gymnastics).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Go/Bubbletea&lt;/strong&gt; — slightly more upfront ceremony than Python, but you get the single static binary, and you already know some Go. This is the sweet spot for &lt;em&gt;your&lt;/em&gt; constraints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rust/ratatui&lt;/strong&gt; — most work (borrow checker + more manual TUI plumbing), only worth it if you specifically want it.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;So Go isn’t the hard option — it’s the right one for what you care about. Python only “wins” on raw speed-to-prototype, and it loses the thing you explicitly said you like.&lt;/p&gt;&lt;p&gt;One more decision inside the Go path that affects difficulty:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Shell out to &lt;code&gt;kubectl&lt;/code&gt;&lt;/strong&gt; (run &lt;code&gt;exec.Command(&quot;kubectl&quot;, ...)&lt;/code&gt;) — much simpler. You let the user’s existing kubeconfig/context do its thing, and you just pipe tar streams around. This keeps you firmly in the “1–2 day prototype” zone.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;client-go&lt;/code&gt; directly&lt;/strong&gt; — no kubectl dependency, cleaner streaming, but &lt;code&gt;client-go&lt;/code&gt; has a real learning curve (the exec/SPDY/remotecommand setup is notoriously fiddly, and kubeconfig loading has its own boilerplate). This is what pushes things toward the week.&lt;/p&gt;&lt;p&gt;My suggestion: start by shelling out to &lt;code&gt;kubectl&lt;/code&gt;. You get a working tool fast, and you can swap the transfer layer for &lt;code&gt;client-go&lt;/code&gt; later without touching the TUI — if you keep a clean interface like:&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; Transfer&lt;/span&gt;&lt;span&gt; interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;) ([]&lt;/span&gt;&lt;span&gt;FileInfo&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Pull&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;remotePath&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;localPath&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;localPath&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;remotePath&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then the kubectl-shelling implementation and a future client-go implementation are just two structs satisfying the same interface.&lt;/p&gt;&lt;p&gt;Want me to put together a starter Bubbletea skeleton with the two-panel layout and a kubectl-backed &lt;code&gt;Transfer&lt;/code&gt; implementation?&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;can you create a PLAN file that I can pass to a coding agent?&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;At that point, Claude created a plan artifact, which I read through and addressed my concerns about certain details.&lt;/p&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;one question about the numeric UIDs. when the pod container runs rootless, you probably can not change UIDs?&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;After Claude clarified, I asked it to update the plan, and then I was ready to start coding.&lt;/p&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;PLAN.md:&lt;/summary&gt;&lt;div&gt;&lt;h1&gt;PLAN: &lt;code&gt;k8tc&lt;/code&gt; — A Two-Panel TUI for Kubernetes Pod File Transfer&lt;/h1&gt;&lt;h2&gt;Goal&lt;/h2&gt;&lt;p&gt;Build a terminal UI tool, written in &lt;strong&gt;Go&lt;/strong&gt;, that lets a user browse the local
filesystem and a Kubernetes pod’s filesystem side-by-side in a two-panel
(Midnight Commander style) layout, and transfer files/directories between them.
Transfers use &lt;strong&gt;&lt;code&gt;tar&lt;/code&gt; streamed over &lt;code&gt;kubectl exec&lt;/code&gt;&lt;/strong&gt; so that timestamps and file
permissions are preserved.&lt;/p&gt;&lt;p&gt;Ship as a &lt;strong&gt;single static binary&lt;/strong&gt;.&lt;/p&gt;&lt;h2&gt;Non-Goals (v1)&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;client-go&lt;/code&gt; integration. v1 shells out to the user’s existing &lt;code&gt;kubectl&lt;/code&gt;.
(Keep the transfer layer behind an interface so &lt;code&gt;client-go&lt;/code&gt; can be added later
without touching the TUI.)&lt;/li&gt;
&lt;li&gt;No editing of remote files in-place.&lt;/li&gt;
&lt;li&gt;No multi-pod parallel transfers.&lt;/li&gt;
&lt;li&gt;No Windows-specific polish (target Linux/macOS; it may work on Windows but
that is not a v1 requirement).&lt;/li&gt;
&lt;/ul&gt;&lt;hr&gt;&lt;h2&gt;Tech Stack&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Language:&lt;/strong&gt; Go (1.22+)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TUI framework:&lt;/strong&gt; &lt;a href=&quot;https://github.com/charmbracelet/bubbletea&quot;&gt;Bubble Tea&lt;/a&gt;
(&lt;code&gt;github.com/charmbracelet/bubbletea&lt;/code&gt;) with
&lt;a href=&quot;https://github.com/charmbracelet/bubbles&quot;&gt;Bubbles&lt;/a&gt; components and
&lt;a href=&quot;https://github.com/charmbracelet/lipgloss&quot;&gt;Lip Gloss&lt;/a&gt; for styling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External dependency at runtime:&lt;/strong&gt; &lt;code&gt;kubectl&lt;/code&gt; must be on the user’s &lt;code&gt;PATH&lt;/code&gt;
and configured (valid kubeconfig / current context). The target pod must have
&lt;code&gt;tar&lt;/code&gt; available in the chosen container.&lt;/li&gt;
&lt;/ul&gt;&lt;hr&gt;&lt;h2&gt;Architecture&lt;/h2&gt;&lt;h3&gt;Package layout&lt;/h3&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;cmd/k8tc/main.go        # entrypoint, flag parsing, bubbletea program start&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;internal/transfer/      # the Transfer interface + kubectl implementation&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;transfer.go           # interface + shared types (FileInfo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;kubectl.go            # kubectl-backed implementation&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;internal/local/         # local filesystem browsing (List/Stat helpers)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;internal/ui/            # bubbletea model, panels, key handling, rendering&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;model.go&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;panel.go&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;keys.go&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;styles.go&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Core interface&lt;/h3&gt;&lt;p&gt;The transfer layer is abstracted so the kubectl implementation can later be
swapped for a &lt;code&gt;client-go&lt;/code&gt; one:&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; transfer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; FileInfo&lt;/span&gt;&lt;span&gt; struct&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    Name    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    Size    &lt;/span&gt;&lt;span&gt;int64&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    Mode    &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;    // e.g. &quot;drwxr-xr-x&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    IsDir   &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ModTime &lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Time&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; Transfer&lt;/span&gt;&lt;span&gt; interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // List returns directory contents at path inside the pod.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    List&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;container&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;) ([]&lt;/span&gt;&lt;span&gt;FileInfo&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // Pull copies remotePath (file or dir) from the pod to localPath, preserving metadata.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    Pull&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;container&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;remotePath&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;localPath&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;progress&lt;/span&gt;&lt;span&gt; func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt; int64&lt;/span&gt;&lt;span&gt;)) &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // Push copies localPath (file or dir) into the pod at remotePath, preserving metadata.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    Push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;container&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;localPath&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;remotePath&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;progress&lt;/span&gt;&lt;span&gt; func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt; int64&lt;/span&gt;&lt;span&gt;)) &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The local filesystem panel does &lt;strong&gt;not&lt;/strong&gt; go through &lt;code&gt;Transfer&lt;/code&gt;; it uses the
&lt;code&gt;internal/local&lt;/code&gt; helpers directly. Only the remote panel uses &lt;code&gt;Transfer&lt;/code&gt;.&lt;/p&gt;&lt;hr&gt;&lt;h2&gt;Transfer Mechanics (the important bit)&lt;/h2&gt;&lt;p&gt;All remote operations shell out to &lt;code&gt;kubectl&lt;/code&gt;. Build commands with
&lt;code&gt;os/exec.CommandContext&lt;/code&gt; and stream stdin/stdout — &lt;strong&gt;never&lt;/strong&gt; buffer whole files
in memory.&lt;/p&gt;&lt;h3&gt;Listing remote files&lt;/h3&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;kubectl exec &amp;lt;pod&amp;gt; [-c &amp;lt;container&amp;gt;] -- ls -la --full-time &amp;lt;path&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Parse the output into &lt;code&gt;[]FileInfo&lt;/code&gt;. Notes:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;--full-time&lt;/code&gt; (GNU coreutils) for a parseable ISO timestamp. If that fails
(BusyBox), fall back to &lt;code&gt;ls -la&lt;/code&gt; and accept coarser/absent mtimes rather than
erroring out.&lt;/li&gt;
&lt;li&gt;Skip the &lt;code&gt;total N&lt;/code&gt; first line.&lt;/li&gt;
&lt;li&gt;Always synthesize a &lt;code&gt;..&lt;/code&gt; entry for navigation (unless at &lt;code&gt;/&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Detect directories from the leading &lt;code&gt;d&lt;/code&gt; in the mode string.&lt;/li&gt;
&lt;/ul&gt;&lt;h3&gt;Pull (pod → local), metadata-preserving&lt;/h3&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;kubectl exec &amp;lt;pod&amp;gt; [-c &amp;lt;container&amp;gt;] -- tar cf - -C &amp;lt;remoteParent&amp;gt; &amp;lt;remoteBase&amp;gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;| tar xpf - --no-same-owner -C &amp;lt;localDest&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tar c&lt;/code&gt; on the remote side, piped to &lt;code&gt;tar xp&lt;/code&gt; locally (&lt;code&gt;-p&lt;/code&gt; preserves mode +
mtime). &lt;code&gt;--no-same-owner&lt;/code&gt; is the default for pulling — see “tar flags &amp;amp;
ownership” below for why.&lt;/li&gt;
&lt;li&gt;Run the local &lt;code&gt;tar&lt;/code&gt; via &lt;code&gt;exec.Command&lt;/code&gt; and connect the kubectl stdout to its
stdin with an &lt;code&gt;io.Pipe&lt;/code&gt; (or &lt;code&gt;cmd.StdoutPipe()&lt;/code&gt; → &lt;code&gt;cmd2.Stdin&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Wrap the pipe in a counting &lt;code&gt;io.Reader&lt;/code&gt; to drive the &lt;code&gt;progress&lt;/code&gt; callback.&lt;/li&gt;
&lt;/ul&gt;&lt;h3&gt;Push (local → pod), metadata-preserving&lt;/h3&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;tar cf - -C &amp;lt;localParent&amp;gt; &amp;lt;localBase&amp;gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;| kubectl exec -i &amp;lt;pod&amp;gt; [-c &amp;lt;container&amp;gt;] -- tar xpf - --no-same-owner -C &amp;lt;remoteDest&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;Note the &lt;code&gt;-i&lt;/code&gt; on &lt;code&gt;kubectl exec&lt;/code&gt; so stdin is forwarded.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--no-same-owner&lt;/code&gt; again by default — in a rootless container the extract
cannot chown anyway (see below); this makes the intent explicit and avoids
warnings.&lt;/li&gt;
&lt;li&gt;Same counting-reader trick for progress.&lt;/li&gt;
&lt;/ul&gt;&lt;h3&gt;tar flags &amp;amp; ownership&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Mode bits and mtime are reliably preserved without privilege. Owner UID/GID
is not — treat it as best-effort.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;When &lt;code&gt;tar x&lt;/code&gt; runs without &lt;code&gt;CAP_CHOWN&lt;/code&gt; (extracting on your local machine as a
normal user, or inside a rootless pod), the &lt;code&gt;chown()&lt;/code&gt; calls fail with &lt;code&gt;EPERM&lt;/code&gt;.
GNU tar’s default for a non-root extract is to &lt;em&gt;silently drop&lt;/em&gt; ownership restore
and create files owned by the extracting user — it does &lt;strong&gt;not&lt;/strong&gt; hard-fail. So a
blanket &lt;code&gt;--numeric-owner&lt;/code&gt; on extract buys nothing in the common case: it only
controls &lt;em&gt;how&lt;/em&gt; a UID is chosen (by number vs. name lookup), not whether tar is
&lt;em&gt;allowed&lt;/em&gt; to apply it.&lt;/p&gt;&lt;p&gt;Defaults, therefore:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Create side:&lt;/strong&gt; &lt;code&gt;tar --numeric-owner -cf - ...&lt;/code&gt;
Numeric is harmless here and avoids name-lookup surprises when packing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extract side (default):&lt;/strong&gt; &lt;code&gt;tar -xpf - --no-same-owner ...&lt;/code&gt;
Preserves mode + mtime, and explicitly tells tar not to attempt chown. This is
the right default for both directions:&lt;/li&gt;
&lt;li&gt;Pulling to local: you almost never want the pod’s UIDs applied on your
machine anyway (UID 1000 in the pod ≠ you).&lt;/li&gt;
&lt;li&gt;Pushing to a rootless pod: the chown would no-op regardless, so don’t pretend
otherwise.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Opt-in ownership preservation:&lt;/strong&gt; add a &lt;code&gt;--preserve-ownership&lt;/code&gt; flag to &lt;code&gt;k8tc&lt;/code&gt;.
When set, use &lt;code&gt;tar --same-owner --numeric-owner -xpf - ...&lt;/code&gt; on the extract side.
This only does anything useful when the extracting end is privileged (root in
the container, or root locally); otherwise it degrades to the same best-effort
behavior. Document this clearly so users aren’t surprised when UIDs don’t carry
across into a rootless target.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;What actually hard-fails&lt;/strong&gt; is unrelated to ownership: writing into a directory
you lack write permission for, or a restored directory mode that locks tar out
mid-extract. Those surface as &lt;code&gt;EPERM&lt;/code&gt;/&lt;code&gt;EACCES&lt;/code&gt; on the file ops themselves and
should be reported per-transfer (see Error Handling).&lt;/p&gt;&lt;hr&gt;&lt;h2&gt;TUI Behavior&lt;/h2&gt;&lt;h3&gt;Layout&lt;/h3&gt;&lt;p&gt;Two equal-width panels filling the terminal, a header line, and a footer/status
line.&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;┌─ LOCAL: /home/user/project ──┐┌─ POD nginx-abc:/var/www ──────┐&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│ ..                           ││ ..                             │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│ &amp;gt; src/                       ││   index.html                   │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   README.md                  ││   assets/                      │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   go.mod                     ││                                │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│                              ││                                │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└──────────────────────────────┘└────────────────────────────────┘&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Tab: switch  ↑↓: move  ⏎: open  F5: copy  q: quit      [status...]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;focused&lt;/strong&gt; panel has a highlighted border; the cursor row is highlighted.&lt;/li&gt;
&lt;li&gt;Each panel maintains its own &lt;code&gt;cwd&lt;/code&gt;, file list, cursor index, and scroll
offset.&lt;/li&gt;
&lt;/ul&gt;&lt;h3&gt;Keybindings&lt;/h3&gt;




































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Key&lt;/th&gt;&lt;th&gt;Action&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Tab&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Switch focus between local and remote panel&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;↑&lt;/code&gt; / &lt;code&gt;↓&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Move cursor&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;PgUp/PgDn&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Page cursor&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Enter&lt;/code&gt;&lt;/td&gt;&lt;td&gt;If dir: descend; if &lt;code&gt;..&lt;/code&gt;: go up; if file: no-op (v1)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;F5&lt;/code&gt; / &lt;code&gt;c&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Copy highlighted entry from focused panel → other panel’s cwd&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;r&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Refresh focused panel&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;q&lt;/code&gt; / &lt;code&gt;Ctrl+C&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Quit&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;h3&gt;Async transfers&lt;/h3&gt;&lt;p&gt;Transfers must not block the event loop. Use the Bubble Tea pattern:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;On &lt;code&gt;F5&lt;/code&gt;, dispatch a &lt;code&gt;tea.Cmd&lt;/code&gt; that runs the &lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; in a goroutine and
returns a &lt;code&gt;transferDoneMsg{err}&lt;/code&gt; (and intermediate &lt;code&gt;transferProgressMsg{n}&lt;/code&gt;
via a channel + &lt;code&gt;tea.Tick&lt;/code&gt; or a custom message pump).&lt;/li&gt;
&lt;li&gt;While in flight, show progress/byte-count in the status line and disable
further copy actions.&lt;/li&gt;
&lt;li&gt;On completion, refresh the destination panel and clear status.&lt;/li&gt;
&lt;/ul&gt;&lt;hr&gt;&lt;h2&gt;CLI&lt;/h2&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;k8tc --pod &amp;lt;name&amp;gt; [--namespace &amp;lt;ns&amp;gt;] [--container &amp;lt;name&amp;gt;] [--remote-path &amp;lt;path&amp;gt;] [--local-path &amp;lt;path&amp;gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--pod&lt;/code&gt; (required for v1)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--namespace&lt;/code&gt; / &lt;code&gt;-n&lt;/code&gt; → passed through as &lt;code&gt;kubectl -n&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--container&lt;/code&gt; / &lt;code&gt;-c&lt;/code&gt; → passed through as &lt;code&gt;kubectl exec -c&lt;/code&gt;; if omitted, let
kubectl pick the default container&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--remote-path&lt;/code&gt; initial remote dir (default &lt;code&gt;/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--local-path&lt;/code&gt; initial local dir (default &lt;code&gt;.&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--preserve-ownership&lt;/code&gt; attempt to restore owner UID/GID on extract
(&lt;code&gt;--same-owner --numeric-owner&lt;/code&gt;). Off by default; only effective when the
extracting end is privileged. See “tar flags &amp;amp; ownership.”&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;(Stretch: a pod picker if &lt;code&gt;--pod&lt;/code&gt; is omitted, via &lt;code&gt;kubectl get pods -o json&lt;/code&gt;.)&lt;/p&gt;&lt;hr&gt;&lt;h2&gt;Error Handling &amp;amp; Edge Cases&lt;/h2&gt;&lt;p&gt;The agent must handle these explicitly, surfacing errors in the status line
rather than crashing:&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;kubectl&lt;/code&gt; not found on PATH&lt;/strong&gt; → fail fast at startup with a clear message.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tar&lt;/code&gt; missing in the pod&lt;/strong&gt; (distroless/scratch images) → detect the exec
failure and show: “pod has no &lt;code&gt;tar&lt;/code&gt;; cannot transfer.” Do &lt;strong&gt;not&lt;/strong&gt; hang.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-container pod with no &lt;code&gt;--container&lt;/code&gt;&lt;/strong&gt; → kubectl will error; surface
its message and hint to pass &lt;code&gt;-c&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permission denied&lt;/strong&gt; on read (local or remote) → show per-transfer error,
keep the UI alive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BusyBox &lt;code&gt;ls&lt;/code&gt;&lt;/strong&gt; lacking &lt;code&gt;--full-time&lt;/code&gt; → fall back gracefully (see Listing).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Broken pipe / context cancel&lt;/strong&gt; mid-transfer → clean up both processes
(&lt;code&gt;CommandContext&lt;/code&gt; + &lt;code&gt;cmd.Wait()&lt;/code&gt; on both ends; kill the partner on failure).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Empty directories&lt;/strong&gt; and the root &lt;code&gt;/&lt;/code&gt; (no &lt;code&gt;..&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Large files&lt;/strong&gt; → never read fully into memory; always stream.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spaces / special chars in paths&lt;/strong&gt; → pass paths as separate &lt;code&gt;exec.Command&lt;/code&gt;
args (no shell string interpolation); when piping two &lt;code&gt;exec.Cmd&lt;/code&gt;s, do it in
Go via pipes, not via a &lt;code&gt;sh -c &quot;... | ...&quot;&lt;/code&gt; string.&lt;/li&gt;
&lt;/ol&gt;&lt;hr&gt;&lt;h2&gt;Suggested Build Order (milestones)&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Transfer interface + kubectl &lt;code&gt;List&lt;/code&gt;.&lt;/strong&gt; CLI prints a remote &lt;code&gt;ls&lt;/code&gt;. Verify
parsing against a real pod.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local &lt;code&gt;List&lt;/code&gt;.&lt;/strong&gt; Mirror the same &lt;code&gt;FileInfo&lt;/code&gt; for the local FS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Static two-panel render&lt;/strong&gt; (Lip Gloss) with both panels populated, no
interaction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Navigation:&lt;/strong&gt; focus switching, cursor movement, &lt;code&gt;Enter&lt;/code&gt; to descend/ascend,
scroll offset, refresh.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Pull&lt;/code&gt; (pod → local)&lt;/strong&gt; synchronous first, then move it onto the async
&lt;code&gt;tea.Cmd&lt;/code&gt; pattern with a status line.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Push&lt;/code&gt; (local → pod)&lt;/strong&gt; same shape as Pull.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Progress reporting&lt;/strong&gt; via counting reader → status line.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge-case hardening&lt;/strong&gt; from the list above (tar-missing, busybox ls,
cancellation).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Polish:&lt;/strong&gt; styling, help footer, &lt;code&gt;--namespace&lt;/code&gt;/&lt;code&gt;--container&lt;/code&gt; plumbing.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;Milestones 1–6 are the usable prototype. 7–9 are the path to “done.”&lt;/p&gt;&lt;hr&gt;&lt;h2&gt;Acceptance Criteria&lt;/h2&gt;&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt; Launches with &lt;code&gt;k8tc --pod &amp;lt;p&amp;gt;&lt;/code&gt; and shows local + remote panels.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt; Tab switches focus; arrows + Enter navigate both filesystems.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt; F5 copies the highlighted file &lt;strong&gt;or directory&lt;/strong&gt; in the focused panel into
the other panel’s current directory.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt; Transferred files retain original mtime and permission (mode) bits
(verify with &lt;code&gt;stat&lt;/code&gt; on both ends). Owner UID/GID is best-effort: preserved
only with &lt;code&gt;--preserve-ownership&lt;/code&gt; against a privileged extract target.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt; Directory transfers are recursive and also preserve metadata.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt; A transfer of a large file does not freeze the UI and shows progress.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt; Missing &lt;code&gt;tar&lt;/code&gt; in the pod produces a clear error, not a hang or panic.&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt; Builds to a single static binary: &lt;code&gt;CGO_ENABLED=0 go build&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;&lt;hr&gt;&lt;h2&gt;Future (post-v1, do not build now)&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Swap the kubectl-backed &lt;code&gt;Transfer&lt;/code&gt; for a &lt;code&gt;client-go&lt;/code&gt; implementation
(exec via &lt;code&gt;remotecommand&lt;/code&gt; SPDY) to drop the &lt;code&gt;kubectl&lt;/code&gt; runtime dependency.&lt;/li&gt;
&lt;li&gt;Pod/namespace picker UI.&lt;/li&gt;
&lt;li&gt;Multi-select and queued transfers.&lt;/li&gt;
&lt;li&gt;File preview / view pane.&lt;/li&gt;
&lt;li&gt;Delete / rename / mkdir operations.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;p&gt;The main takeaway here is not to start the implementation in the chat. That’s the entirely wrong environment. Get a &lt;strong&gt;PLAN artifact&lt;/strong&gt; and take it to the code environment.&lt;/p&gt;
&lt;h2&gt;Step 2: Initial Implementation&lt;/h2&gt;
&lt;p&gt;It was time to move to Claude Code. I run Claude in a &lt;a href=&quot;https://gist.github.com/splitbrain/5b20b82abfcd874c0b5b6ed22cb40443&quot;&gt;custom sandbox&lt;/a&gt; using bubblewrap on Linux. This allows me to &lt;strong&gt;safely run in &lt;code&gt;auto&lt;/code&gt; mode&lt;/strong&gt;, which means Claude is running mostly autonomously with an LLM classifier as the only safeguard.&lt;/p&gt;
&lt;p&gt;In this concrete example, the sandbox prevented Claude from accessing my kubectl config and thus from running kubectl commands against our cluster unchecked.&lt;/p&gt;
&lt;p&gt;I added the &lt;code&gt;PLAN.md&lt;/code&gt; to a new repository and let Claude one-shot the initial implementation.&lt;/p&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;read @PLAN.md and implement it&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;About 20 minutes later, Claude reported that it was done. Time to give the tool a quick test.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cosmocode.de/en/_astro/k8tc-initial.CWqxJFxk_Z22WC5H.webp&quot; alt=&quot;Initial implementation&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1145&quot; height=&quot;674&quot;&gt;&lt;/p&gt;
&lt;p&gt;It worked just as specified ✨️. I could have stopped here if this were an one-off tool.&lt;/p&gt;
&lt;h2&gt;Step n: Review&lt;/h2&gt;
&lt;p&gt;Before I decided to add more features, I wanted to review the code.&lt;/p&gt;
&lt;p&gt;Another reason why I decided to use Go is that the code is usually more straightforward and easier to review than Python or Rust. Go tends to have only one way to do things, which makes it easier for the agent to generate code that is correct and idiomatic. And that in turn makes it easier for me to review the code and understand what it does.&lt;/p&gt;
&lt;p&gt;When vibe engineering, I mostly &lt;strong&gt;look at architectural decisions and code structure&lt;/strong&gt;. I don’t want to nitpick every line of code, but I want to make sure that the overall design makes sense and that the code is organized in a way that is maintainable.&lt;/p&gt;
&lt;p&gt;As an example, here is the review session I had with Claude right after the initial implementation.&lt;/p&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;Let’s review the current state. I see that we have a FileInfo struct and a Transfer interface. The comment says that this only covers the remote side and that the local side does not use the interface. But then looking at local.go it uses transfer.FileInot and transfer.Sort so it seems the two sides are not that different?&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude gives an explanation that doesn’t fully satisfy me&lt;/summary&gt;&lt;div&gt;&lt;p&gt;I read all four files. Here’s what’s actually going on — the tension you spotted is real but it’s conflating two different things that live in the &lt;code&gt;transf er&lt;/code&gt; package.&lt;/p&gt;&lt;h2&gt;There are two separate things in &lt;code&gt;transfer.go&lt;/code&gt;&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A shared data layer&lt;/strong&gt; — the &lt;code&gt;FileInfo&lt;/code&gt; struct (transfer.go:15) and the &lt;code&gt;Sort&lt;/code&gt; helper (transfer.go:40). Both panels use these, by design. That’s why &lt;code&gt; FileInfo&lt;/code&gt;’s own comment says “on either the local or the remote side.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;Transfer&lt;/code&gt; interface&lt;/strong&gt; — &lt;code&gt;List&lt;/code&gt;/&lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; (transfer.go:25). &lt;em&gt;This&lt;/em&gt; is what the local side doesn’t use.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;The comment at transfer.go:23-24 is talking strictly about #2. And it’s literally accurate: the local panel calls &lt;code&gt;local.List(p)&lt;/code&gt; — a plain package functi
on (model.go:109) — whereas the remote panel dispatches through the interface, &lt;code&gt;t.List(...)&lt;/code&gt; (model.go:111). So when the comment says “does not go through
this interface,” it means local doesn’t implement the &lt;code&gt;List/Pull/Push&lt;/code&gt; &lt;em&gt;method set&lt;/em&gt;, not that local is walled off from the package.&lt;/p&gt;&lt;p&gt;So your instinct is &lt;strong&gt;half right&lt;/strong&gt;: the &lt;em&gt;display&lt;/em&gt; side is barely different (shared types + shared sort), but the &lt;em&gt;transfer&lt;/em&gt; side is fundamentally asymmetr
ic.&lt;/p&gt;&lt;h2&gt;Why local isn’t behind the interface&lt;/h2&gt;&lt;p&gt;Two reasons, and the second is the deeper one:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nothing to swap.&lt;/strong&gt; The package doc says the interface exists “so the kubectl implementation can later be swapped for a client-go one.” The local files
ystem has no such alternative — it’s always &lt;code&gt;os.ReadDir&lt;/code&gt;. An interface with exactly one possible implementation is just ceremony.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The local side has no &lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; of its own.&lt;/strong&gt; Look at the signatures: &lt;code&gt;Pull(pod, container, remotePath, localPath, ...)&lt;/code&gt; and &lt;code&gt;Push(pod, container,  localPath, remotePath, ...)&lt;/code&gt; — the &lt;code&gt;Kubectl&lt;/code&gt; value &lt;em&gt;already straddles both ends&lt;/em&gt;; it reads/writes local paths itself (it’s the local &lt;code&gt;tar&lt;/code&gt; process in &lt;code&gt;pi pe&lt;/code&gt;). So &lt;code&gt;Transfer&lt;/code&gt; isn’t really “the remote filesystem” — it’s “the thing that bridges local↔remote.” The only purely-local operation is &lt;em&gt;listing a direc
tory&lt;/em&gt;, and that’s a one-liner over &lt;code&gt;os.ReadDir&lt;/code&gt;. There’s no “local Transfer” concept to even abstract.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;That’s the real answer to “are the two sides that different?”: for browsing, no; for transferring, there is no local side — it’s subsumed into &lt;code&gt;Kubectl&lt;/code&gt;.&lt;/p&gt;&lt;h2&gt;Two minor warts worth noting in the review&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;FileInfo&lt;/code&gt;/&lt;code&gt;Sort&lt;/code&gt; live in the package named after the remote side&lt;/strong&gt;, yet are used by both. It’s pragmatic (local imports transfer, not vice versa), but it’s why the comment has to keep clarifying “this is also used locally.” If you wanted stricter cohesion you’d hoist &lt;code&gt;FileInfo&lt;/code&gt;+&lt;code&gt;Sort&lt;/code&gt; into a small shared package (e.g. &lt;code&gt;fileinfo&lt;/code&gt;) that both &lt;code&gt;transfer&lt;/code&gt; and &lt;code&gt;local&lt;/code&gt; import — but that’s arguably over-engineering for two callers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;..&lt;/code&gt;/root-synthesis logic is duplicated&lt;/strong&gt; — local.go:36-38 vs kubectl.go:139-141. The duplication is &lt;em&gt;justified&lt;/em&gt;: local uses &lt;code&gt;filepath&lt;/code&gt; (OS separators) and remote uses &lt;code&gt;path&lt;/code&gt; (always &lt;code&gt;/&lt;/code&gt;, inside a Linux pod), so they can’t trivially share it. But it’s worth a one-line comment on each noting the deliberate split, since they’ll need to stay in sync.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;If you want, I can tighten the interface comment to say “the &lt;code&gt;List&lt;/code&gt;/&lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; &lt;em&gt;method set&lt;/em&gt;” instead of “this interface” (to kill the ambiguity you hit), or sketch the &lt;code&gt;fileinfo&lt;/code&gt; extraction so you can judge whether it’s worth it. Which direction do you want to take?&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;I feel like if there are two concerns (transfer and file representation) then the two should be separated. It would make it less weird that local reuses the file representation then&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude refactors and mentions another issue&lt;/summary&gt;&lt;div&gt;&lt;h2&gt;Decisions I made&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Renamed &lt;code&gt;FileInfo&lt;/code&gt; → &lt;code&gt;file.Info&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;file.FileInfo&lt;/code&gt; would stutter; &lt;code&gt;file.Info&lt;/code&gt; is the idiomatic Go form. &lt;code&gt;Sort&lt;/code&gt; keeps its name (&lt;code&gt;file.Sort&lt;/code&gt;). If you’d
rather preserve the &lt;code&gt;FileInfo&lt;/code&gt; name, say so and I’ll rename — it’s a mechanical change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reworded the &lt;code&gt;Transfer&lt;/code&gt; doc comment&lt;/strong&gt; to describe it as a local↔remote &lt;em&gt;bridge&lt;/em&gt; (List reads the pod; Pull/Push move trees between pod and disk), which
is the accurate framing we landed on in the previous discussion — and it no longer needs to awkwardly disclaim the shared type, since the type now visibl
y lives in its own package.&lt;/li&gt;
&lt;/ul&gt;&lt;h2&gt;Still untouched (deliberately)&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;..&lt;/code&gt;/root-synthesis duplication between &lt;code&gt;local.List&lt;/code&gt; and &lt;code&gt;parseLS&lt;/code&gt; is still there — that’s the &lt;code&gt;filepath&lt;/code&gt;-vs-&lt;code&gt;path&lt;/code&gt; split we discussed, and it’s ortho
gonal to this refactor. &lt;code&gt;file&lt;/code&gt; would be the natural home for a shared helper if you ever want to collapse it, but I left it alone since it’d need the sepa
rator parameterized. Want me to tackle that too?&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;I don’t remember discussing filepath-vs-path - what’s that about?&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude explains the issue and recommends to ignore it&lt;/summary&gt;&lt;div&gt;&lt;p&gt;Fair — I overstated it as “discussed.” It was a one-line aside in my earlier review, not something we actually worked through. Here’s what I meant.&lt;/p&gt;&lt;p&gt;It’s about two different standard-library packages in Go:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;path&lt;/code&gt;&lt;/strong&gt; — manipulates always-slash-separated paths (&lt;code&gt;/&lt;/code&gt;). It’s what you use for URLs and anything that is &lt;em&gt;defined&lt;/em&gt; to use forward slashes regardless of the host OS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;path/filepath&lt;/code&gt;&lt;/strong&gt; — manipulates paths using the &lt;em&gt;host OS’s&lt;/em&gt; rules: &lt;code&gt;/&lt;/code&gt; on Linux/macOS, &lt;code&gt;\&lt;/code&gt; on Windows.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;In k8tc the two panels live on different sides of that line:&lt;/p&gt;&lt;p&gt;The &lt;strong&gt;local&lt;/strong&gt; panel walks the filesystem of whatever machine k8tc runs on, so it correctly uses &lt;code&gt;filepath&lt;/code&gt;:&lt;/p&gt;&lt;p&gt;That last grep is the clearest illustration: &lt;code&gt;kubectl.go&lt;/code&gt; uses &lt;strong&gt;both&lt;/strong&gt; packages, deliberately. The remote (pod) side uses &lt;code&gt;path&lt;/code&gt; (lines 141, 149–150) and the local side of the very same transfer uses &lt;code&gt;filepath&lt;/code&gt; (lines 158–159).&lt;/p&gt;&lt;p&gt;The reason: a pod is a Linux container, so its filesystem paths are &lt;em&gt;always&lt;/em&gt; &lt;code&gt;/&lt;/code&gt;-separated, no matter what OS k8tc itself is running on. If you used &lt;code&gt;filepath&lt;/code&gt; on a pod path while running k8tc on Windows, you’d get backslash semantics applied to a path that is genuinely slash-based — wrong. So pod paths must go through &lt;code&gt;path&lt;/code&gt;; host paths must go through &lt;code&gt;filepath&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Now, the &lt;code&gt;..&lt;/code&gt;-synthesis duplication I flagged. The two snippets are:&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// local.go:36 — host filesystem&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; filepath.&lt;/span&gt;&lt;span&gt;Clean&lt;/span&gt;&lt;span&gt;(dir) &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;(filepath.Separator) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// kubectl.go:141 — pod filesystem&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; path.&lt;/span&gt;&lt;span&gt;Clean&lt;/span&gt;&lt;span&gt;(dir) &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &quot;/&quot;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;They’re structurally identical (“if this isn’t the root, prepend a &lt;code&gt;..&lt;/code&gt; entry”), but they differ in exactly the two spots where the path-flavor matters:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;which &lt;code&gt;Clean&lt;/code&gt;&lt;/strong&gt; — OS-aware vs. always-slash&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;what counts as root&lt;/strong&gt; — &lt;code&gt;string(filepath.Separator)&lt;/code&gt; (the host’s root) vs. the literal &lt;code&gt;&quot;/&quot;&lt;/code&gt; (the pod’s root)&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;So you &lt;em&gt;can’t&lt;/em&gt; just lift the snippet into one shared function and call it from both sides — you’d have to parameterize it with “which path semantics,” e.g. pass in the clean-func and the root sentinel. That’s why I called the duplication “justified”: collapsing it would trade two obvious 3-line blocks for one indirection-laden helper, and arguably make it &lt;em&gt;less&lt;/em&gt; clear that one side is host-pathed and the other is pod-pathed.&lt;/p&gt;&lt;p&gt;So nothing for you to do here — I just wanted to flag that the duplication is intentional, not an oversight, in case it caught your eye during review. It’s unrelated to the &lt;code&gt;file&lt;/code&gt; extraction we just did.&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;Hmm to me it sound like local and remote should implement at least Transfer.List (don’t implement, discuss)&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude suggests different ways to address the issue&lt;/summary&gt;&lt;div&gt;&lt;p&gt;Good instinct — and it points at a real seam. Let me lay out the case for and against, because there’s a genuine obstacle that’s worth seeing clearly. First let me confirm one thing the argument hinges on:&lt;/p&gt;&lt;p&gt;Confirmed what I needed: &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt; are set once in &lt;code&gt;New&lt;/code&gt; (model.go:66–67) and only ever read after that. They’re &lt;strong&gt;session-constant&lt;/strong&gt;, yet they’re threaded as arguments into every single &lt;code&gt;List&lt;/code&gt;/&lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; call. That detail is the whole crux of your suggestion. Here’s the discussion.&lt;/p&gt;&lt;h2&gt;Your instinct is right — for &lt;code&gt;List&lt;/code&gt; specifically&lt;/h2&gt;&lt;p&gt;The two operations in &lt;code&gt;Transfer&lt;/code&gt; have fundamentally different &lt;em&gt;shapes&lt;/em&gt;:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;List&lt;/code&gt;&lt;/strong&gt; is &lt;strong&gt;symmetric and one-sided&lt;/strong&gt;: &lt;code&gt;(path) → []file.Info&lt;/code&gt;. Both panels do &lt;em&gt;exactly&lt;/em&gt; this. There’s nothing pod-specific about the operation itself — “read a directory, give me the entries.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt;&lt;/strong&gt; are &lt;strong&gt;asymmetric and two-sided&lt;/strong&gt;: they move a tree &lt;em&gt;across&lt;/em&gt; the local↔pod boundary. There is no “local Pull”; the kubectl process is inherently both ends at once. These genuinely belong only to the bridge.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;So &lt;code&gt;Transfer&lt;/code&gt; today actually bundles two responsibilities — &lt;em&gt;browsing&lt;/em&gt; and &lt;em&gt;moving&lt;/em&gt; — and you’ve spotted that browsing is the half that’s symmetric. This is the same kind of cut we just made pulling &lt;code&gt;file&lt;/code&gt; out: separate the thing that’s shared from the thing that isn’t.&lt;/p&gt;&lt;h2&gt;The obstacle: the signature carries &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;Why local &lt;em&gt;can’t&lt;/em&gt; satisfy &lt;code&gt;Transfer.List&lt;/code&gt; as written:&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Kubectl: &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;(pod, container, path &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;) ([]file.Info, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)   &lt;/span&gt;&lt;span&gt;// 3 path-ish args&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;local:   &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;(path &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;)                 ([]file.Info, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)   &lt;/span&gt;&lt;span&gt;// 1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The local side has no pod or container, so it can’t match. But — as the grep above shows — &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt; are session-constant. They don’t &lt;em&gt;need&lt;/em&gt; to be per-call args; they’re only per-call because the interface made them so. Bind them once and the signatures converge:&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; Lister&lt;/span&gt;&lt;span&gt; interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    List&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;) ([]&lt;/span&gt;&lt;span&gt;file&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Info&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;local&lt;/strong&gt;: a zero-field &lt;code&gt;local.FS{}&lt;/code&gt; whose &lt;code&gt;List(path)&lt;/code&gt; wraps today’s &lt;code&gt;local.List&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;remote&lt;/strong&gt;: a tiny adapter holding &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt;, whose &lt;code&gt;List(path)&lt;/code&gt; calls &lt;code&gt;kubectl.List(pod, container, path)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;&lt;h2&gt;What it buys, concretely&lt;/h2&gt;&lt;p&gt;&lt;code&gt;loadPanel&lt;/code&gt; loses its &lt;code&gt;which == focusLocal&lt;/code&gt; fork (model.go:108–112):&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// before&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; which &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; focusLocal {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    files, err &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; local.&lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;(p)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    files, err &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; t.&lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;(pod, container, p)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// after — each Panel owns its own lister&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;files, err &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; m.&lt;/span&gt;&lt;span&gt;panelPtr&lt;/span&gt;&lt;span&gt;(which).lister.&lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;(p)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Each &lt;code&gt;Panel&lt;/code&gt; becomes self-sufficient: it knows how to load &lt;em&gt;itself&lt;/em&gt;, and the Model stops caring whether a panel is local or remote when listing. That’s a real improvement in the model’s coherence, and it makes test fakes trivial — a &lt;code&gt;Lister&lt;/code&gt; fake is one method, versus today’s &lt;code&gt;fakeTransfer&lt;/code&gt; that must stub &lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; even for a listing test.&lt;/p&gt;&lt;h2&gt;Where I’d put the interface&lt;/h2&gt;&lt;p&gt;The idiomatic Go move: &lt;strong&gt;define &lt;code&gt;Lister&lt;/code&gt; in the consumer (&lt;code&gt;ui&lt;/code&gt;), not the producer.&lt;/strong&gt; Then &lt;code&gt;local.FS&lt;/code&gt; satisfies it structurally without importing anything, and &lt;code&gt;local&lt;/code&gt; stays fully decoupled (no &lt;code&gt;transfer&lt;/code&gt; import — which we just worked to remove). The remote adapter can live in &lt;code&gt;ui&lt;/code&gt; too. &lt;code&gt;transfer.Transfer&lt;/code&gt; keeps &lt;code&gt;List&lt;/code&gt; as the &lt;em&gt;backend&lt;/em&gt; capability (the future client-go backend must still list), and the adapter just curries &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt; onto it.&lt;/p&gt;&lt;h2&gt;Two honest caveats&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It doesn’t unify everything.&lt;/strong&gt; &lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; stay asymmetric and the push-vs-pull decision stays in the UI (model.go:289/292). The &lt;code&gt;Lister&lt;/code&gt; cut only simplifies &lt;em&gt;browsing&lt;/em&gt;. That’s fine — but don’t expect it to collapse the transfer logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It doesn’t touch the &lt;code&gt;..&lt;/code&gt;/&lt;code&gt;filepath&lt;/code&gt;-vs-&lt;code&gt;path&lt;/code&gt; duplication.&lt;/strong&gt; Each side’s &lt;code&gt;List&lt;/code&gt; still synthesizes &lt;code&gt;..&lt;/code&gt; its own way. Orthogonal to this.&lt;/li&gt;
&lt;/ol&gt;&lt;h2&gt;My recommendation&lt;/h2&gt;&lt;p&gt;Worth doing, at the &lt;strong&gt;lightweight&lt;/strong&gt; end: introduce a consumer-side &lt;code&gt;Lister&lt;/code&gt;, adapt both sides, give each &lt;code&gt;Panel&lt;/code&gt; its own. It’s contained, reversible, and improves the conceptual model.&lt;/p&gt;&lt;p&gt;I’d &lt;em&gt;stop short&lt;/em&gt; of the maximal version (ripping &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt; out of the &lt;code&gt;Transfer&lt;/code&gt; interface entirely and binding them at &lt;code&gt;Kubectl&lt;/code&gt; construction). That’s arguably “more correct” — it’d kill the per-call threading on &lt;code&gt;Pull&lt;/code&gt;/&lt;code&gt;Push&lt;/code&gt; too — but it turns &lt;code&gt;Kubectl&lt;/code&gt; from a reusable engine into a session-scoped object, and the blast radius is much larger for a payoff that’s mostly aesthetic.&lt;/p&gt;&lt;p&gt;The deciding question is really: do you want each &lt;code&gt;Panel&lt;/code&gt; to “own how it lists itself”? If yes, the &lt;code&gt;Lister&lt;/code&gt; is the clean expression of that. If the 6-line branch in &lt;code&gt;loadPanel&lt;/code&gt; doesn’t bother you, the status quo is defensible too — the asymmetry it encodes (only the remote side is swappable) is real.&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;TBH your “maximal version” sounds more correct. I feel like there are distinct responsibilities. A kubernetes package which wraps around kubectl and handling the tar commands. A ls parser (used probably remotely only). A Transfer manager that implements the push/pull mechanism. And remote/local implementations of the same “virtual” file system list. Would you agree?&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-claude&quot;&gt;&lt;details&gt;&lt;summary&gt;Claude comes up with a detailed proposal for what I suggested&lt;/summary&gt;&lt;div&gt;&lt;p&gt;Yes — broadly I agree. Those are real, separable responsibilities, and the fact that you can name them this cleanly is a good sign the seams are real and
not invented. But I’d refine &lt;em&gt;one&lt;/em&gt; of your four boundaries, and in doing so I think the decomposition gets sharper than what you sketched. Let me give you the version I’d actually defend.&lt;/p&gt;&lt;h2&gt;The dependency graph I’d aim for&lt;/h2&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;file        Info + Sort                                    (done; depends on nothing)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ▲   ▲&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  │   └──────────────┐&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;parser              local Lister        kube      ← the swap seam&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;(ls -la → []Info)   (os.ReadDir)         ▲  ▲&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ▲                                      │  │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └────── remote Lister ─────────────────┘  │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          (ls via kube + parse)             │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                  transfer (Pull/Push)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                  (tar streaming over kube)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;file&lt;/code&gt;&lt;/strong&gt; — representation. Done.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;kube&lt;/code&gt;&lt;/strong&gt; — the exec/stream primitive: “run this command in this pod/container, give me stdin/stdout/stderr.” Holds the bound &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;container&lt;/code&gt; (killing the per-call threading you spotted). Knows nothing about tar, ls, or &lt;code&gt;file.Info&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;parser&lt;/strong&gt; — pure &lt;code&gt;[]byte → []file.Info&lt;/code&gt;. Remote-only, as you said (locally &lt;code&gt;os.ReadDir&lt;/code&gt; already hands back structured entries; nothing to parse).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;two Listers&lt;/strong&gt; — local (&lt;code&gt;os.ReadDir&lt;/code&gt;) and remote (&lt;code&gt;kube&lt;/code&gt; + parser), both satisfying one &lt;code&gt;List(path) → []file.Info&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;transfer&lt;/code&gt;&lt;/strong&gt; — the Pull/Push manager: tar create/extract + the streaming pipe, using &lt;code&gt;kube&lt;/code&gt; for the remote endpoint and local &lt;code&gt;exec&lt;/code&gt; for the local one.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;No cycles, clean DAG. Each node has an independent test story — and notably the tests you &lt;em&gt;already have&lt;/em&gt; fall out naturally: &lt;code&gt;parse_test.go&lt;/code&gt; (pure parser), and &lt;code&gt;fakeKubectl&lt;/code&gt; (which makes the “pod” just the local fs) is really a test of the transfer streaming independent of real k8s.&lt;/p&gt;&lt;h2&gt;Where I’d push back: tar does &lt;em&gt;not&lt;/em&gt; belong in the &lt;code&gt;kube&lt;/code&gt; package&lt;/h2&gt;&lt;p&gt;You said the kubernetes package wraps kubectl &lt;em&gt;and&lt;/em&gt; the tar commands. I’d split those. Put &lt;strong&gt;only the exec/stream primitive&lt;/strong&gt; in &lt;code&gt;kube&lt;/code&gt;; keep &lt;strong&gt;tar&lt;/strong&gt; (createArgs/extractArgs/&lt;code&gt;pipe&lt;/code&gt;/classify) in the &lt;code&gt;transfer&lt;/code&gt; manager.&lt;/p&gt;&lt;p&gt;The reason is your own swappability rationale. The package doc promises “swap the kubectl implementation for a client-go one without touching the TUI.” The thing that differs between kubectl and client-go is &lt;em&gt;how you exec into a pod and get a byte stream&lt;/em&gt; — not tar. tar runs identically either way. So the swap seam is the exec primitive, and it must be tar-agnostic, or every transfer detail leaks across the seam. If &lt;code&gt;kube&lt;/code&gt; owns tar, you haven’t isolated the swappable part; you’ve just renamed &lt;code&gt;Kubectl&lt;/code&gt;.&lt;/p&gt;&lt;h2&gt;The non-obvious consequence — and why this is the version that makes the doc &lt;em&gt;true&lt;/em&gt;&lt;/h2&gt;&lt;p&gt;Here’s the part I think is the real payoff, and it’s hidden in &lt;code&gt;pipe&lt;/code&gt;:&lt;/p&gt;&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; pipe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;src&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;dst&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Cmd&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;progress&lt;/span&gt;&lt;span&gt; func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;int64&lt;/span&gt;&lt;span&gt;)) &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;pipe&lt;/code&gt; wires together &lt;strong&gt;two &lt;code&gt;*exec.Cmd&lt;/code&gt;&lt;/strong&gt;. That’s fine for kubectl (both endpoints are local processes — &lt;code&gt;kubectl exec …&lt;/code&gt; is itself a local process). But &lt;strong&gt;client-go does not give you an &lt;code&gt;*exec.Cmd&lt;/code&gt;&lt;/strong&gt; — it gives you a &lt;code&gt;remotecommand&lt;/code&gt; executor you hand &lt;code&gt;Stdin&lt;/code&gt;/&lt;code&gt;Stdout&lt;/code&gt;/&lt;code&gt;Stderr&lt;/code&gt; io streams. So today’s &lt;code&gt;Transfer&lt;/code&gt; interface &lt;em&gt;claims&lt;/em&gt; client-go swappability while its internals (two &lt;code&gt;*exec.Cmd&lt;/code&gt;) would actively fight it. The swappability is currently aspirational, not real.&lt;/p&gt;&lt;p&gt;The decomposition you’re proposing fixes exactly that — if the &lt;code&gt;kube&lt;/code&gt; seam is expressed as streams (&lt;code&gt;Run(cmd, stdin io.Reader, stdout, stderr io.Writer) error&lt;/code&gt;) rather than &lt;code&gt;*exec.Cmd&lt;/code&gt;, then &lt;code&gt;pipe&lt;/code&gt; connects two &lt;em&gt;stream endpoints&lt;/em&gt;, the local tar side is one impl, the pod side is another, and kubectl-vs-client-go becomes a true drop-in. So this isn’t just tidying — it’s the refactor that would make the package doc’s promise honest. That’s the strongest argument &lt;em&gt;for&lt;/em&gt; going maximal.&lt;/p&gt;&lt;h2&gt;Two cautions&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Granularity.&lt;/strong&gt; Four responsibilities ≠ four packages. The parser is ~45 lines of pure code — I’d keep it as &lt;code&gt;parse.go&lt;/code&gt; &lt;em&gt;inside&lt;/em&gt; the remote-listing package with its existing test, not a package of its own. A package per function is its own smell. The boundaries that earn a package are &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;kube&lt;/code&gt;, &lt;code&gt;local&lt;/code&gt;, &lt;code&gt;transfer&lt;/code&gt;, and the remote side; whether remote-List and Pull/Push are one “pod” package or two is a coin-flip I’d decide on cohesion (I lean: one &lt;code&gt;pod&lt;/code&gt;/&lt;code&gt;remote&lt;/code&gt; package exposing both, since they share the &lt;code&gt;kube&lt;/code&gt; client and the “pod-ness” — but your capability-split is defensible).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost is front-loaded in &lt;code&gt;pipe&lt;/code&gt;.&lt;/strong&gt; Everything else (extract &lt;code&gt;kube&lt;/code&gt;, bind pod/container, add Listers) is mechanical and low-risk. Re-expressing &lt;code&gt;pipe&lt;/code&gt; around streams instead of &lt;code&gt;*exec.Cmd&lt;/code&gt; is the one genuinely fiddly change, and it only pays off the day someone actually writes the client-go backend. If that day is hypothetical, you can stop short: extract &lt;code&gt;kube&lt;/code&gt; as an &lt;code&gt;*exec.Cmd&lt;/code&gt; factory (still removes the threading, still separates concerns) and leave &lt;code&gt;pipe&lt;/code&gt; alone. You lose nothing you have today and can tighten the seam later.&lt;/li&gt;
&lt;/ol&gt;&lt;h2&gt;So, sequencing&lt;/h2&gt;&lt;p&gt;If you want to do this, I’d go in dependency order, each step independently shippable and green:&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;Extract &lt;strong&gt;&lt;code&gt;kube&lt;/code&gt;&lt;/strong&gt; (exec primitive, pod/container bound) — removes the per-call threading.&lt;/li&gt;
&lt;li&gt;Introduce the &lt;strong&gt;&lt;code&gt;Lister&lt;/code&gt;&lt;/strong&gt; interface + local/remote impls — collapses the &lt;code&gt;loadPanel&lt;/code&gt; branch.&lt;/li&gt;
&lt;li&gt;Move &lt;strong&gt;&lt;code&gt;transfer&lt;/code&gt;&lt;/strong&gt; to Pull/Push-only on top of &lt;code&gt;kube&lt;/code&gt;; isolate the &lt;strong&gt;parser&lt;/strong&gt; into the remote lister.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Optional, later:&lt;/em&gt; re-express &lt;code&gt;pipe&lt;/code&gt;/&lt;code&gt;kube&lt;/code&gt; around streams when (if) client-go becomes real.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;Net: I agree with your four-way split, with the single amendment that tar lives with &lt;code&gt;transfer&lt;/code&gt;, not &lt;code&gt;kube&lt;/code&gt; — and the headline is that doing this is what would finally make the “swap to client-go” sentence in the package doc actually true rather than aspirational.&lt;/p&gt;&lt;p&gt;Want me to start at step 1?&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;/blockquote&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;I like your analysis and corrections. It seems like a sound plan. Implement the changes fully&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;The final result was a lot cleaner than the original first version. At the same time, I had gotten a rough overview of the codebase.&lt;/p&gt;
&lt;p&gt;As you can see, I often try to give Claude &lt;strong&gt;open-ended questions&lt;/strong&gt; to coax it to “think” through the problem and come up with its own analysis, rather than just asking for a specific change. Only when I am confident that I have understood the issue and solution well enough do I come in with specific guidance. A &lt;code&gt;question:&lt;/code&gt; prefix or an explicit &lt;code&gt;discuss, don&#039;t code&lt;/code&gt; can keep it from jumping to code before the design is clear without having to switch back to planning mode.&lt;/p&gt;
&lt;p&gt;The example above also shows the importance of &lt;strong&gt;pushing back&lt;/strong&gt; on the agent’s suggestions when they don’t feel right. Claude often tries to take the lazy way out ;-)&lt;/p&gt;
&lt;h2&gt;Step n+1: Add Features&lt;/h2&gt;
&lt;p&gt;With a first prototype and a clearer understanding of the codebase, I can now start adding features. I won’t go through all the details here, but the process is similar to what we did before: identify a feature, discuss it with Claude, and then ask it to implement it.&lt;/p&gt;
&lt;blockquote class=&quot;chat chat-me&quot;&gt;&lt;p&gt;Currently only a single item can be copied. I would like to be able to mark multiple files or directories and then copy them over. Before the copy starts, a confirmation dialog should be shown. While the copy is running a progress dialog (with abort button) should be shown instead of using the status line for progress tracking. Can you come up with a plan first?&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Once a feature is planned and implemented, it’s a good time to review the code again and make more architectural improvements if needed. Repeat until you’re happy.&lt;/p&gt;
&lt;p&gt;I usually start a new session for each feature and for each review.&lt;/p&gt;
&lt;h2&gt;Final Result&lt;/h2&gt;
&lt;p&gt;The final tool is available at its &lt;a href=&quot;https://github.com/cosmocode/k8tc&quot;&gt;GitHub repository&lt;/a&gt;. Following the commits, you can see the sequence of adding new features and refactorings in between.&lt;/p&gt;
&lt;p&gt;The final tool has the following features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;selecting multiple files and directories&lt;/li&gt;
&lt;li&gt;copying files and directories&lt;/li&gt;
&lt;li&gt;deleting files and directories&lt;/li&gt;
&lt;li&gt;creating new directories&lt;/li&gt;
&lt;li&gt;confirmation dialogs for copy and delete operations&lt;/li&gt;
&lt;li&gt;abortable progress dialog for copy and delete operations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I might add a pod/container selector on startup in the future, but for now it’s doing exactly what I wanted it to do, and the whole process took about four hours of work. Incidentally, writing this blog post took nearly the same amount of time ;-)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cosmocode.de/en/_astro/k8tc-final.DYVF5lhV_K7k0t.webp&quot; alt=&quot;Final result&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1145&quot; height=&quot;674&quot;&gt;&lt;/p&gt;</description>
            <pubDate>Tue, 09 Jun 2026 00:00:00 +0000</pubDate>
            <guid>https://www.cosmocode.de/en/blog/agoh/20260609-vibe-engineering/</guid>
        </item>
        <item>
            <title>Stop Using Conventional Commits - Sumner Evans</title>
            <link>https://sumnerevans.com/posts/software-engineering/stop-using-conventional-commits/</link>
            <description>&lt;blockquote&gt;&lt;hr&gt;&lt;/blockquote&gt;&lt;div&gt;&lt;p&gt;You’ve almost certainly encountered
&lt;a href=&quot;https://www.conventionalcommits.org/en/v1.0.0/&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;Conventional Commits&lt;/a&gt; before.
It may have reared its ugly head in the changelog of an open source project
you’ve used. It may have been the enforced commit format for an open source
project you contributed to. A lot of people swear by it. I swear &lt;em&gt;at&lt;/em&gt; it.&lt;/p&gt;&lt;p&gt;Even though it is used by
&lt;a href=&quot;https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;a&lt;/a&gt;
&lt;a href=&quot;https://electronjs.org/docs/development/pull-requests#commit-message-guidelines&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;large&lt;/a&gt;
&lt;a href=&quot;https://contribute.freecodecamp.org/how-to-contribute-to-the-codebase/&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;number&lt;/a&gt;
&lt;a href=&quot;https://jenkins-x.io/community/code/&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;of&lt;/a&gt;
&lt;a href=&quot;https://github.com/conventional-changelog/commitlint/blob/master/.github/CONTRIBUTING.md&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;popular&lt;/a&gt;
&lt;a href=&quot;https://github.com/semantic-release/semantic-release/blob/master/CONTRIBUTING.md&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;open&lt;/a&gt;
&lt;a href=&quot;https://github.com/nuxt/nuxt/blob/main/CONTRIBUTING.md&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;source&lt;/a&gt;
&lt;a href=&quot;https://github.com/vitejs/vite/blob/main/.github/commit-convention.md&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;projects&lt;/a&gt;,
Conventional Commits is an actively bad standard which
&lt;strong&gt;&lt;a href=&quot;#focus-failure&quot;&gt;encourages focus on the wrong things&lt;/a&gt; and
&lt;a href=&quot;#broken-promises&quot;&gt;fails to deliver on its promises&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;&lt;h2 id=&quot;focus-failure&quot;&gt;Focus Failure
&lt;a href=&quot;#focus-failure&quot; aria-label=&quot;Permalink to Focus Failure&quot;&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Conventional Commits promises to add semantic meaning to commit messages to aid
developers and end-users in understanding the changes made in a commit. However,
Conventional Commits fails to do this in spectacular fashion. To demonstrate
this, let’s look at the anatomy of a conventional commit. According to the
&lt;a href=&quot;https://www.conventionalcommits.org/en/v1.0.0/#summary&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;Conventional Commit website&lt;/a&gt;
commit messages should be formatted as follows:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex=&quot;0&quot;&gt;&lt;code data-lang=&quot;patch&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;lt;type&amp;gt;[optional scope]: &amp;lt;description&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;[optional body]
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;[optional footer(s)]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The commit’s subject line has a &lt;code&gt;&amp;lt;type&amp;gt;&lt;/code&gt; (something like &lt;code&gt;fix&lt;/code&gt;, &lt;code&gt;feat&lt;/code&gt;, &lt;code&gt;chore&lt;/code&gt;,
&lt;code&gt;docs&lt;/code&gt;, or &lt;code&gt;refactor&lt;/code&gt;&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;) describing the type of change. Following that, there
is an optional scope, and then a description.&lt;/p&gt;&lt;p&gt;This format has a major failing: &lt;strong&gt;type is prioritised over scope&lt;/strong&gt;. This is
exactly backwards.&lt;/p&gt;&lt;h3 id=&quot;scope--type&quot;&gt;Scope &amp;gt; Type
&lt;a href=&quot;#scope--type&quot; aria-label=&quot;Permalink to Scope &gt; Type&quot;&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The &lt;em&gt;scope&lt;/em&gt; of a change (the subject of the change) is the most important part
of a commit. To demonstrate this, let’s consider why each one of the following
stakeholders care about the scope of the change more than the type of the
change:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Contributors:&lt;/strong&gt; when you are a contributor to a project, you often need to
read the commit log to identify changes in the codebase relevant to a certain
area of the code. There are many reasons for this including:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Wanting to catch up on what has happened since the last time you
contributed.&lt;/li&gt;&lt;li&gt;Trying to understand where the project’s overall inertia is.&lt;/li&gt;&lt;li&gt;Looking for commits that might conflict with your in-progress work when
pulling or rebasing.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;As you read the commit log, you’re looking at &lt;em&gt;what areas&lt;/em&gt; were touched. You
really do not care about the &lt;em&gt;type&lt;/em&gt; of change happening, you care about the
&lt;em&gt;scope&lt;/em&gt; of the change.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debuggers:&lt;/strong&gt; when investigating a bug, you often want to look through the
commit log to see what changes might have touched areas related to the
component where the bug manifested. Once again, the scope is the most
important piece of information. The type of change is entirely useless because
bugs can be introduced in any change regardless of type. (I’m sure we’ve all
experienced writing a bugfix that caused another bug.)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Incident responders:&lt;/strong&gt; when production is down, scanning the commit log for
changes that were made around the time of the outage is an effective way to
identify what areas may be causing the problem. Scope is once again the most
important piece of information you can have at this point. For example, if you
see a commit related to the &lt;code&gt;auth&lt;/code&gt; scope at the tip of the spike of inbound
API errors, it’s a likely culprit for the problem. And once again, type is
irrelevant because bugs could have been added by any change.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So what does Conventional Commits do? It deprioritises scope so much that it’s
&lt;em&gt;optional&lt;/em&gt;! Why the hell is &lt;em&gt;scope&lt;/em&gt; optional? Having a commit without a scope is
like having a sentence without a subject! Then, to add insult to injury,
Conventional Commits elevates &lt;em&gt;type&lt;/em&gt; to the front of the commit message.
Conventional Commits gets the priority of scope and type entirely wrong.&lt;/p&gt;&lt;h3 id=&quot;type-is-redundant-and-restrictive&quot;&gt;Type is Redundant and Restrictive
&lt;a href=&quot;#type-is-redundant-and-restrictive&quot; aria-label=&quot;Permalink to Type is Redundant and Restrictive&quot;&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;You might be thinking “so it may be backwards, but commit type is at least still
important, right?” and to that I say “no”. A commit’s description should almost
always tell you the type of the change! Consider
&lt;a href=&quot;https://github.com/angular/angular/commit/ec138c3645f6e28829e69b6da2a839c248bb3bf0&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;this commit message&lt;/a&gt;
as an example:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex=&quot;0&quot;&gt;&lt;code data-lang=&quot;patch&quot;&gt;&lt;span&gt;&lt;span&gt;fix(compiler): prevent namespaced SVG &amp;lt;style&amp;gt; elements from being stripped
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Even if you only had the description, it’s obvious that it was a bugfix! Space
on the subject line of a commit is already at a premium, wasting characters on
the type is not helpful! But it’s often even worse than useless; it’s often
restrictive. Take
&lt;a href=&quot;https://github.com/angular/angular/commit/683172b39a602ac9ec15db69d22853433a67a084&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;this commit message&lt;/a&gt;
as an example:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex=&quot;0&quot;&gt;&lt;code data-lang=&quot;patch&quot;&gt;&lt;span&gt;&lt;span&gt;refactor(core): Update webmcp support to use document.modelContext
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This commit updated the &lt;code&gt;webmcp&lt;/code&gt; functionality in the &lt;code&gt;core&lt;/code&gt; component to
support both &lt;code&gt;document.modelContext&lt;/code&gt; and &lt;code&gt;navigator.modelContext&lt;/code&gt;, so was that a
bugfix, refactor, or new feature? I would argue it’s all of them! But again, the
only thing that really matters is that it was a change to the &lt;code&gt;core/webmcp&lt;/code&gt;
component.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Conventional Commits fundamentally focuses on the wrong thing (the commit
type) and devalues the scope (which is what people actually care about).&lt;/strong&gt;&lt;/p&gt;&lt;h2 id=&quot;broken-promises&quot;&gt;Broken Promises
&lt;a href=&quot;#broken-promises&quot; aria-label=&quot;Permalink to Broken Promises&quot;&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;So we have determined that the format of Conventional Commits sucks, but it must
provide &lt;em&gt;some&lt;/em&gt; benefit. Let’s read the
&lt;a href=&quot;https://www.conventionalcommits.org/en/v1.0.0/#why-use-conventional-commits&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;&lt;em&gt;Why Use Conventional Commits&lt;/em&gt;&lt;/a&gt;
section to see if any of the reasons make any sense.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatically generating CHANGELOGs.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This is the biggest promise of Conventional Commits: you can run a tool like
&lt;a href=&quot;https://git-cliff.org/&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;git-cliff&lt;/a&gt; or
&lt;a href=&quot;https://github.com/conventional-changelog/conventional-changelog&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;conventional-changelog&lt;/a&gt;
to generate a changelog from the commits since your last release. Is this even
a good idea? No! The audience of a changelog is entirely different than the
audience for a commit log!&lt;/p&gt;&lt;p&gt;A changelog is user-facing, and the user cares about understanding the
functional differences between versions. They care about what changed from a
&lt;strong&gt;business/functional&lt;/strong&gt; perspective.&lt;/p&gt;&lt;p&gt;A commit log is developer-facing, and the developers care about reading a
story of how the codebase has changed over time. They care about what changed
from a &lt;strong&gt;scope&lt;/strong&gt; perspective.&lt;/p&gt;&lt;p&gt;As you can see, these are two entirely different grains, and any efforts to
combine them result in subpar results. The reasons for this are multiple:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;In any moderately complex project, it takes multiple commits to land any
notable feature. The process of landing the feature (as documented by the
commit log) is valuable for developers and contributors, but it’s useless
for the end-user. The end-user only cares about the new feature, not how it
was built!&lt;/li&gt;&lt;li&gt;As
&lt;a href=&quot;https://richvdh.org/conventional-commits-considered-harmful.html#reverts-are-hard&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;Rich pointed out&lt;/a&gt;,
reverts are problematic for Conventional Commits. Revert commits are
important from a commit log story perspective for developers, but to the end
user, a change that is reverted is equivalent to a change not made.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatically determining a semantic version bump (based on the types of
commits landed).&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This sounds nice, but the realities of software engineering often interfere
significantly with the viability of accurately accomplishing this task.
Consider the following situations:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Reverts:&lt;/strong&gt; imagine a situation where the breaking change you introduced
was actually so breaking that you have to revert it? Your tooling will pick
up a breaking change and increment the major version even though the
breakage was actually reverted and there is no breaking change.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Accidental breakages:&lt;/strong&gt; maybe the breakage is subtle and you don’t realise
a change is a breaking change when you make the change. Only in retrospect
realise that it’s breaking. You will incorrectly increment a minor/patch
version when a major version bump is necessary.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Retroactive unbreakages:&lt;/strong&gt; say you later add a commit which, in
composition with a previously breaking commit, results in a diff which is
not breaking. Similar to the revert situation, tooling would incorrectly
identify a breaking change.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In such situations, you could rewrite history with a rebase, but that often
breaks or is prevented by workflows. It also presents a revisionist history to
the contributors trying to contribute to the project, reducing the reliability
of the story the commit log is telling.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Communicating the nature of changes to teammates, the public, and other
stakeholders.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;As we have established up to this point, teammates and the public have very
different needs from a changelog and commit log. Conventional Commits manages
to solve neither.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Triggering build and publish processes.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This is just a bad idea. Say you only run automated security checks on commits
that touch code and then someone creates a Trojan-horse commit titled
&lt;code&gt;docs: fix typos&lt;/code&gt; which actually introduces vulnerabilities into the
authentication subsystem? Obviously, that sort of malicious activity would
hopefully be caught in code review, but the automated tooling is bypassed,
putting the onus on a human to identify the problem.&lt;/p&gt;&lt;p&gt;Compute is cheap, just use &lt;code&gt;git diff&lt;/code&gt; to identify changed files (scope, once
again) and run build/publish processes based on that.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Making it easier for people to contribute to your projects, by allowing them
to explore a more structured commit history.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;More structured, sure. Making it easier to contribute? Not at all (as we have
already demonstrated at length).&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Not a single one of the “selling points” for Conventional Commits actually holds
water.&lt;/p&gt;&lt;p&gt;Conventional Commits is also extremely difficult to apply to a project. You are
supposed to define your own set of “types”, but pretty much everyone just takes
the defaults from
&lt;a href=&quot;https://github.com/conventional-changelog/commitlint&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;commitlint&lt;/a&gt; which often
don’t fit well with the particulars of individual projects. This problem is
especially acute in corporate environments where change management and audit
requirements often mandate a ticket number in every commit message. The
&lt;code&gt;&amp;lt;scope&amp;gt;&lt;/code&gt; field is the obvious place to put it, but this ends up replacing the
only useful metadata in a Conventional Commit with a completely useless ticket
number.&lt;/p&gt;&lt;h2 id=&quot;a-better-way&quot;&gt;A Better Way
&lt;a href=&quot;#a-better-way&quot; aria-label=&quot;Permalink to A Better Way&quot;&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;So what should you do instead? Follow the lead of truly successful software
projects like Linux, FreeBSD, Git, Go, and NixOS! What do these projects have in
common? They all use scope-prefixed commit messages (where “scope” is defined to
be relevant to the actual project). Usually, the scope to use on a given project
is self-evident. For the Linux kernel, the subsystem is the natural scope. For
Go projects, the package path is the natural scope. For a project using a
microservice architecture, the microservice name is the natural scope.&lt;/p&gt;&lt;p&gt;Here are some examples of projects and their commit format guidelines.&lt;/p&gt;&lt;table readabilityDataTable=&quot;1&quot;&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Project&lt;/th&gt;&lt;th&gt;Format&lt;/th&gt;&lt;th&gt;Example&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://www.kernel.org/doc/html/v4.14/process/submitting-patches.html&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;Linux&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;subsystem: description&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/torvalds/linux/commit/1d774589f924&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;i2c: virtio: mark device ready before registering the adapter&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://freebsdfoundation.org/wp-content/uploads/2020/11/Writing-Commit-Messages.pdf&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;FreeBSD&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;prefix: Description&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/freebsd/freebsd-src/commit/f77d37cffdf3&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;linuxulator: Return EINVAL for invalid inotify flags&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://git-scm.com/docs/SubmittingPatches&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;Git&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;area: description&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/git/git/commit/62319b49bbe7&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;gitlab-ci: update macOS image&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://go.dev/wiki/CommitMessage&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;Go&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;package: description&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/golang/go/commit/517d4d3c7976&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;net/http/cookiejar: add godoc links&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;nixpkgs&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;pkg-name: description&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/NixOS/nixpkgs/commit/7bf858875a54&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;xwayland: 24.1.11 -&amp;gt; 24.1.12&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/nodejs/node/blob/main/doc/contributing/pull-requests.md#commit-message-guidelines&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;Node.js&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;subsystem: description&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/nodejs/node/commit/5f727fdc89c06782652bfbf6a4d05ade1db3d2c8&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;stream: fast-path stateless transform flush results&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Unfortunately, despite being used by some of the most successful open source
projects ever created, this commit style seems to have lost the branding war. I
intend to change that. Introducing
&lt;a href=&quot;https://scopedcommits.com&quot; title=&quot;(opens in new tab)&quot; rel=&quot;external&quot; target=&quot;_blank&quot;&gt;scopedcommits.com&lt;/a&gt;. The website is dedicated to
advocating for a return to commit message sanity, and separating the concern of
changelog generation from commit log management.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion
&lt;a href=&quot;#conclusion&quot; aria-label=&quot;Permalink to Conclusion&quot;&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Conventional Commits’ purported advantages are actually illusory and the
industry has seen no tangible benefit from using it as a standard. However,
Conventional Commits unfortunately seems to have become fairly popular in open
source projects, and due to this it seems like AIs have a habit of defaulting to
using it for commit messages. This has caused propagation of anti-pattern-ridden
commit messages across projects.&lt;/p&gt;&lt;p&gt;My goal in this article is to fight against Conventional Commits’ dominance, and
demonstrate that there better ways to structure commit messages. But if this
article has not convinced you to stop using Conventional Commits, I look forward
to the flame war in the comment section.&lt;/p&gt;&lt;/div&gt;</description>
            <pubDate>Sat, 06 Jun 2026 04:53:43 +0000</pubDate>
            <guid>https://sumnerevans.com/posts/software-engineering/stop-using-conventional-commits/</guid>
        </item>
    </channel>
</rss>
