<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>John Saigle</title>
    <description>Software engineering, security, and technical insights</description>
    <link>https://johnsaigle.com</link>
    <atom:link href="https://johnsaigle.com/feed.xml" rel="self" type="application/rss+xml"/>
    <language>en-us</language>
    <lastBuildDate>Sun, 30 Aug 2026 23:03:11 +0000</lastBuildDate>
    <pubDate>Sun, 30 Aug 2026 23:03:11 +0000</pubDate>
    <ttl>60</ttl>
    
    <item>
      <title><![CDATA[Someone Still Has to Read It]]></title>
      <description><![CDATA[Unedited LLM prose saves the writer time by spending the reader&#39;s attention. sloppy-syntax is a small CLI for flagging familiar patterns during writing and review.]]></description>
      <pubDate>Sat, 01 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://johnsaigle.com/posts/someone-still-has-to-read-it</link>
      <guid isPermaLink="true">https://johnsaigle.com/posts/someone-still-has-to-read-it</guid>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 680px;">
          <p>LLMs make it easy to move effort from the writer to the reader.</p>

<p>Someone generates an issue or pull request and sends it without an editing round. They save a few minutes by making every reader sort through the output. This feels like a low-effort, almost anti-social use of the tool. The familiar phrases hit our ears wrong, so our brains preserve attention by rejecting the text. Any useful information buried in the slop goes with it.</p>

<p>I started keeping a list of those phrases in my original <a href="/posts/sloppy-syntax">Sloppy Syntax</a> post. After I saw <a href="https://tools.simonwillison.net/llm-cliche-highlighter">Simon Willison’s LLM cliche highlighter</a>, I realized the worst offenders could be checked quickly with a simple CLI.</p>

<p>So I made <a href="https://github.com/johnsaigle/sloppy-syntax"><code>sloppy-syntax</code></a>, a small Rust program that scans text for recurring LLM cliches and rhetorical patterns. It reports each match in its sentence and gives the text a weighted density score from 1 to 5.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sloppy-syntax <span class="nt">--file</span> essay.txt
</code></pre></div></div>

<p>It is a rule-based linter, not proof that an LLM wrote something. Humans use tired phrases too, and models can avoid them. The useful question is whether the text still needs an editing pass before another person has to read it.</p>

<p>I want this for my own blog. I also want it as a quick filter for drive-by GitHub issues, pull requests, and bug reports padded with synthetic helpfulness. Ideally the writer runs it first. On the consuming side, I see it as triage rather than a gate. It can explain why a wall of apparently polished prose is so hard to pay attention to and give the reviewer a reason to ask for a concise rewrite instead of silently tuning out.</p>

<h2 id="the-mental-antivirus">The mental antivirus</h2>

<p>The tells will change. LLMs will get better at avoiding familiar cliches, while readers get better at recognizing the broader texture of generated prose. It’s a cat-and-mouse game between the models and our mental antivirus.</p>

<p>That reflex has collateral damage. Someone can find a real bug or have a genuinely interesting idea, then use an LLM to help explain it. Stock phrasing smothers the useful work. A reader recognizes the texture, tunes out, and rejects the insight along with the prose.</p>

<p>Can LLM-assisted writing become fresh enough to carry an idea in a voice people want to read? Unsure yet. I’m still experimenting with it. This tool, and this blog as a whole, are attempts to push LLM-assisted writing in that direction. They may fail. I still want to see how far it can go.</p>

        </div>
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Save Your Tokens for Judgment Calls]]></title>
      <description><![CDATA[If code can do it, don&#39;t burn tokens on it. Save the LLM for the parts that actually need judgment.]]></description>
      <pubDate>Thu, 19 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://johnsaigle.com/posts/save-your-tokens-for-judgment-calls</link>
      <guid isPermaLink="true">https://johnsaigle.com/posts/save-your-tokens-for-judgment-calls</guid>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 680px;">
          <p>I once watched an LLM burn through a hundred thousand tokens reviewing code I’d already written static analysis rules for. The model was doing work that a script could do better, faster, and without hallucinating function names. I was paying for something I could have gotten for free.</p>

<p>Most people use LLMs as a runtime. Prompt for everything, every time. Dan Miessler has a better ratio: <a href="https://danielmiessler.com/p/80-20-rule-ai-agents">80% code, 20% LLMs</a>. Code handles the exhaustive, repeatable work; LLMs handle judgment calls over novel inputs. That split eliminates what models do badly and preserves the one thing that actually requires inference.</p>

<h2 id="self-linters">Self-linters</h2>

<p>A self-linter is a project-specific check that no generic tool covers. matklad <a href="https://matklad.github.io/2024/03/22/basic-things.html">writes about this</a> as one of the basic things every serious project needs. TigerBeetle has <a href="https://github.com/tigerbeetle/tigerbeetle/blob/main/src/tidy.zig"><code>tidy.zig</code></a>: no large blobs in git, no merge commits on feature branches, markdown wrapped at 80 columns.</p>

<p>This is where LLMs fit perfectly. You ask once: “What project-specific lint checks should this codebase have?” The model writes the checks, they run in CI forever, and its job is done.</p>

<p>They’re especially good at catching vibe-coding habits. LLMs love generating documentation: README files, CONTRIBUTING guides, markdown scattered across every directory. The drift accumulates fast. They mix casing conventions in filenames—<code>README.md</code> in one directory, <code>readme.markdown</code> in another—and reach for patterns that work in general but go against your project’s style. When you notice yourself correcting the same thing over and over, that’s a lint rule waiting to be written. Catch it at commit time instead of in review.</p>

<p>Project-specific linters encode your project’s actual standards. They’re auditable, version-controlled, reviewable. A prompt is none of those things.</p>

<h2 id="git-hooks-and-small-scripts">Git hooks and small scripts</h2>

<p>“What are useful git hooks for this project?” is a question that produces a set of scripts. The LLM answers once, the hooks fire on every commit, pre-push, post-merge, and after that you never burn another token on it.</p>

<p>Same idea for monitoring. You could ask an LLM “check if any interesting PRs landed on this repo today,” or you could have it write a script that runs <code>gh pr list --json</code>, filters by label and file path, and posts a notification. Put that on a cron job and it never hallucinates a PR number, never costs anything to run.</p>

<p>I use this pattern for tracking changes across bug bounty repositories. A small script calls <code>gh</code> and <code>git</code> commands, scores PRs by security relevance, and surfaces the ones worth reading. The LLM wrote the scoring logic once; the script has executed it thousands of times since without ever touching the model again.</p>

<h2 id="narrowing-the-search-space">Narrowing the search space</h2>

<p>The examples above are all cases where code fully replaces the LLM. The pattern gets more interesting when they work together.</p>

<p>Consider security review. An LLM doing freeform code review makes silent judgment calls about what to focus on—it might decide a component is low-risk because it “just” handles RPC serialization, or skip a function because the surrounding code looks well-structured. These shortcuts are invisible, and they won’t show up in the output or in chain-of-thought reasoning. You can’t audit what the model chose not to look at.</p>

<p>Static analysis inverts this. You write rules that surface every instance of a pattern you care about—unchecked error returns, unsafe type casts, functions that touch user input without validation—and the tool is exhaustive. Every match, every time, regardless of how boring the surrounding code looks.</p>

<p>Hand that list to the LLM and now it’s evaluating a complete, deterministic set of findings instead of deciding what to look at in the first place. The code eliminated the coverage gaps; the LLM provides the judgment static analysis can’t. Is this finding exploitable? Is the context safe? Does the surrounding logic make this a false positive?</p>

<p>Static analysis can’t assess exploitability. LLMs can’t guarantee they checked every file. Together, the coverage is complete.</p>

<p>There’s a trap here, though. Code that narrows the search space also constrains it. If your rules miss a class of vulnerability, the LLM will never see it. You can read the rules. You can’t read the model’s attention. The deterministic side gives you blind spots you can name. That’s better than blind spots you can’t.</p>

<h2 id="tools-that-write-tools">Tools that write tools</h2>

<p>My blog’s content management is a set of rake tasks the LLM wrote: create a draft with slugified filename and frontmatter template, validate structure across all posts, spellcheck against a custom dictionary, import from external sources, publish when ready. Every step has a correct answer and a binary pass/fail, so the LLM authored the pipeline in a few sessions and now it runs on every post without its involvement. (This post went through it.)</p>

<p>These tools won’t last forever. Standards evolve, scripts need updating. That’s a conversation with the LLM every few months, not a prompt on every commit.</p>

<h2 id="the-principle-underneath">The principle underneath</h2>

<p>Most of what people prompt for has a correct answer and a deterministic path to it. Code is cheap, auditable, and never hallucinates. Move that work off the model and you sharpen what’s left: the LLM stops doing grunt work and starts doing the thing it’s actually good at, which is judgment over situations no one wrote a rule for.</p>

<p>The ratio isn’t fixed. Keep moving specifiable work to the code side as you learn what qualifies.</p>

        </div>
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Solana Vulnerabilities That Aren&#39;t]]></title>
      <description><![CDATA[A few bug classes that auditors keep reporting, but which don&#39;t actually exist.]]></description>
      <pubDate>Thu, 12 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://johnsaigle.com/posts/solana-vulnerabilities-that-arent</link>
      <guid isPermaLink="true">https://johnsaigle.com/posts/solana-vulnerabilities-that-arent</guid>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 680px;">
          <p>I published a post with Asymmetric Research about bug classes that keep showing up in audit reports, security courses, and LLM outputs—but which aren’t actually vulnerabilities.</p>

<p>Topics include:</p>

<ul>
  <li>Reentrancy (not a thing in Solana)</li>
  <li>Closed account discriminators (removed from Anchor years ago)</li>
  <li>Float non-determinism (emulated, not hardware-dependent)</li>
  <li>Self-transfers always succeeding (they don’t)</li>
  <li>Partial state commitment (transactions are atomic)</li>
  <li>Unchecked CPI return values (they revert automatically)</li>
</ul>

<p>Getting these right helps everyone focus on real issues. The post explains what changed, why the old advice persists, and how the Solana ecosystem continues to evolve.</p>

<p><a href="https://blog.asymmetric.re/solana-vulnerabilities-that-arent-unpacking-common-misreports/">Read it on the Asymmetric Research blog</a></p>

        </div>
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Getting Roasted by my LLM Style Editor]]></title>
      <description><![CDATA[My own style-editing agent delivered unexpectedly harsh feedback. It stung, and it was exactly what I needed.]]></description>
      <pubDate>Sun, 30 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://johnsaigle.com/posts/getting-roasted-by-my-llm-style-editor</link>
      <guid isPermaLink="true">https://johnsaigle.com/posts/getting-roasted-by-my-llm-style-editor</guid>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 680px;">
          <p>I got caught being lazy. I wanted to experiment by writing a brief, fragmented post at the end of the day. The goal was simply to explain-brag that I’d found a neat trick to get an LLM to do a post-processing step that increased its output quality somewhat.</p>

<p>As is my routine, I sent the rough draft through my “Style Editor” <a href="https://opencode.ai/docs/agents/#subagents">subagent</a>. That’s when it got personal.</p>

<p>Here are some selected lines from the LLM’s review of my prose:</p>
<ul>
  <li><em>“… reads like jargon explaining jargon”</em></li>
  <li><em>“tepid business-speak”</em></li>
  <li><em>“undermines its own point”</em></li>
  <li><em>“the unfinished structure introduces sloppiness (ironically)”</em></li>
</ul>

<p>But this was the kicker:</p>

<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gu">### Does This Need to Be Published?</span>

Your "Sloppy Syntax" post already catalogs LLM output problems. This post adds the <span class="ge">*action*</span> of fixing them, which is valuable, but ask yourself: is this additive to your existing work, or redundant?

If you proceed, make it either comprehensively practical (show the full prompt, give more examples, make it a resource) or philosophically interesting (what does the need for deslopification tell us?). The current draft falls in the exhausting middle ground that you eloquently criticized in "Bernoulli Coding."
</code></pre></div></div>

<p><strong>Damn</strong>. This is a far cry from the fulsome you’re-absolutely-right style feedback I’m used to. Honestly, it stung a little.</p>

<p>It’s also hilarious. I was not expecting to get roasted by the machine. I was unconsciously relying on a bit of the ol’ sycophancy to get me through to the next draft.</p>

<p>I get that this is me playing chess against myself: I’m using my own words to critique my own words. I attempted to approximate my aesthetic preferences, programming a strange machine to tell myself when I don’t like something that I’m doing.</p>

<p>There’s value in ruthless self-assessment, and in coming back to a piece in a different frame of mind from the one you were in during the initial creation. “Write drunk, edit sober.”</p>

<p>I’m used to this way of thinking while writing or making music, but this experience was new. Instead of coming back to the work with a different mindset, I’ve <em>approximated</em> the critical mindset. The shock came from having that receiving the editorial feedback while I was in a lazy mode of the creative mindset.</p>

<p>I’m thankful for it. The Style Editor shook me out of a complacent mindset and genuinely challenged me to put more effort into writing something worthwhile.</p>

<p>The trick, apparently, is stating your aesthetic preferences clearly enough that the system delivers tough love on its own. I told it what good writing looks like, fed it my existing posts, and it somehow decided I needed to be called out. The harshness emerged organically, which makes it more interesting than if I’d explicitly programmed it to roast me.</p>

<p>I’m still entertained by moments like this. It’s fun when these things surprise you, and it’s this feeling of playful<br />
exploration that keeps me motivated to blend LLMs into my writing and coding practices.</p>

        </div>
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Stop Running `npm install`]]></title>
      <description><![CDATA[The sha1-hulud attack compromised 830 packages and leaked 11,000 secrets. MFA stops account takeovers. `npm ci` stops propagation.]]></description>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://johnsaigle.com/posts/stop-running-npm-install</link>
      <guid isPermaLink="true">https://johnsaigle.com/posts/stop-running-npm-install</guid>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 680px;">
          <p>This week, I watched the <a href="https://socket.dev/blog/sha1-hulud-november-2025">sha1-hulud attack</a> spread through the JavaScript ecosystem in real time. Over 830 npm packages compromised. 28,000 repositories infected. More than 11,000 unique secrets leaked, with over 2,000 still valid and publicly exposed as of November 24.</p>

<p>The response focused on securing accounts: enable MFA, audit dependencies, rotate secrets. Necessary, but reactive. There’s a prevention strategy that gets overlooked: stop running <code>npm install</code>.</p>

<h2 id="the-problem">The Problem</h2>

<p><code>npm install</code> pulls whatever version satisfies your semver range. Without a lockfile, you get the latest compatible version published in the last few seconds. With a lockfile, you’re still trusting that maintainer accounts haven’t been compromised, that transitive dependencies are clean, that post-install scripts aren’t malicious.</p>

<p>Every <code>npm install</code> is a bet that nothing malicious has been published since your last install. Sha1-hulud proved that bet loses.</p>

<h2 id="version-pinning-as-defense">Version Pinning as Defense</h2>

<p><a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns">Dependency cooldowns</a> are a valuable concept. (I encountered it on <a href="https://simonwillison.net/2025/Nov/21/dependency-cooldowns/">Simon Willison</a>’s blog this week.) Wait a configurable period after a package is published before allowing it into your project. If a malicious version gets published, you have time to catch it before it reaches production.</p>

<p>For projects with serious security requirements, treat dependency updates as security events. They deserve dedicated review time and careful examination of changelogs. Staying several versions behind might actually reduce risk when attacks exploit the usual advice to patch quickly.</p>

<p>After the last major attack, we added explicit rules to <a href="https://github.com/wormhole-foundation/wormhole/blob/main/CONTRIBUTING.md#supply-chain-security">Wormhole’s Guardian node CONTRIBUTING.md</a>:</p>

<p>For existing projects, use <code>npm ci</code>. While <code>npm install</code> updates dependencies to newer versions within the semver range and might pull down a compromised package published five minutes ago, <code>npm ci</code> installs exactly what’s in <code>package-lock.json</code>.</p>

<p>When adding packages, pin versions. Use <code>npm i package@version</code> to specify exact versions. Specific versions can’t be overwritten after release, which blocks an entire class of attacks.</p>

<p>The same principles apply to Docker containers. Copy <code>package.json</code> and <code>package-lock.json</code> first, then run <code>npm ci</code>. For global packages, specify versions: <code>npm install -g somepackage@1.2.3</code>. Pin base images with SHA-256 hashes: <code>FROM node:18.19.0-alpine@sha256:12345...</code>.</p>

<h2 id="building-lockfile-guard">Building lockfile-guard</h2>

<p>Manual discipline doesn’t scale. Developers will continue using the unsafe default workflow unless something stops them. I wanted a tool that would catch these mistakes before they reached production, so I wrote <a href="https://github.com/johnsaigle/lockfile-guard">lockfile-guard</a>.</p>

<p>It’s a GitHub Action that scans your repository for dangerous package manager commands. It checks Dockerfiles, markdown documentation, and shell scripts. When it finds violations, it fails the build.</p>

<p>The rules:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># ✅ Safe patterns</span>
<span class="s">npm ci</span>
<span class="s">npm i package@version</span>
<span class="s">pnpm install --frozen-lockfile</span>
<span class="s">yarn install --immutable</span>
<span class="s">bun install --frozen-lockfile</span>

<span class="c1"># ❌ Dangerous patterns</span>
<span class="s">npm install</span>
<span class="s">npm i package</span>
<span class="s">pnpm install</span>
<span class="s">yarn install</span>
<span class="s">bun install</span>
</code></pre></div></div>

<p>The output:</p>

<pre><code>✗ ./Dockerfile
  Line 15: Use 'npm ci' instead of 'npm install' for lockfile-based installations
  &gt; npm install

✗ ./.github/workflows/deploy.yml
  Line 42: Use 'pnpm install --frozen-lockfile' to respect lockfile
  &gt; pnpm install
</code></pre>

<p>Nothing advanced here, just pattern matching in files that matter. Simple tools are valuable when they encode protections that might otherwise depend on institutional memory.</p>

<h2 id="trust-is-not-a-security-model">Trust Is Not a Security Model</h2>

<p>We trust that packages do what they claim, that maintainer accounts are protected, that the registry enforces security boundaries. Sha1-hulud exploited that trust. A single compromise rippled through thousands of projects in hours.</p>

<p>Use <code>npm ci</code>. Pin your versions. Audit your dependencies. Harden your CI/CD pipelines with least-privilege access. When the next supply chain attack arrives, and it will, you’ll be ready.</p>

        </div>
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Slop In A Bucket]]></title>
      <description><![CDATA[LLM-generated code, linters, and the security problems we can and can&#39;t solve.]]></description>
      <pubDate>Sun, 23 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://johnsaigle.com/posts/slop-in-a-bucket</link>
      <guid isPermaLink="true">https://johnsaigle.com/posts/slop-in-a-bucket</guid>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 680px;">
          <p>LLM-generated code is everywhere. The speed is seductive, the YOLO mode irresistible. Despite the horrible security problems<br />
this entails, <a href="https://www.youtube.com/watch?v=b6_IZK-1naY">I also like to live <code>--dangerously</code>.</a></p>

<p>I’ve been thinking about whether you can vibe code complex applications and still ship secure code. Here are some lines of<br />
thought I’ve been exploring.</p>

<h2 id="fragment-1-linters-as-llm-guardrails">Fragment 1: Linters as LLM Guardrails</h2>

<p>The main defense is linters and static analysis tools. They’re deterministic, which matters enormously. You can integrate them into CI or commit hooks to block code that doesn’t meet standards. Better yet, high-quality tools often include suggested fixes or even auto-fix flags that can be applied directly to the codebase. With reasonably high-signal information like that, you can run the linter, have the LLM look at the output, and watch it fix its own mistakes.</p>

<p>Ask an LLM to code an app from scratch and run it against common linting tools. You’ll find dozens of errors. The LLM can look at those reports and fix the code it just made. Suddenly, even though it used dangerous patterns, it’s able to correct the ones we can lint for. It’s like having a sloppy chef who’ll clean up after themselves if you point at the spills.</p>

<p><a href="/posts/slopsquatting-detection-added-to-scary-strings">SlopSquatting</a> is interesting here because it’s a new bug class introduced by LLMs while also being a problem we can address via linting.</p>

<h2 id="fragment-2-the-limits-of-linting">Fragment 2: The Limits of Linting</h2>

<p>We need lint rules with high signal-to-noise ratios and suggested fixes. The more patterns we can detect and auto-correct, the better we can constrain generated code. But we get no security guarantees for unlintable problems. Logic errors, for instance, or authentication flows that span multiple files.</p>

<p>Linters raise the floor of quality on a codebase generated by an LLM, but vibe coding is going to remain an insecure process. The main problem is keeping context and intent across several distinct files. It’s easy to mess up an authentication system in a web app because you have to track state across user endpoints, authorization and routes, database schemas, refresh tokens, lockouts, and so on. It’s a bunch of complicated mechanisms glued together, and it’s easy to miss one line or make changes in one set of commits that undoes work in another set.</p>

<h2 id="fragment-3-the-problem-of-the-missing-mind">Fragment 3: The Problem of the Missing Mind</h2>

<p>During a security review of a complex codebase, it’s common to go in and not understand how things work. But you can usually piece together the complex features with enough time and with the implicit trust that someone got this to work well at some point. There’s some sense of order lurking behind even a chaotic codebase. It may be that no single person on the team knows how it all works, but you get the blind men touching the elephant thing. Talk to enough people, read enough documentation, and you can usually assemble the whole from the parts, even if no one person sees the complete picture.</p>

<p>This won’t be the case with LLM-generated code. There is no intent creating a through-line in the implementation. No original architect who made deliberate choices, no accumulated wisdom from code reviews, no debugging sessions that revealed why something had to be done a particular way. Just slop in a bucket, tokens arranged in plausible patterns. The linters catch what they can, but the ghost in the machine isn’t haunted by understanding—just by training data.</p>

<h2 id="fragment-4-everything-is-perl-now">Fragment 4: Everything is Perl Now</h2>

<p>I have a soft spot for Perl despite its shortcomings. Maybe I love it because of what other people consider its shortcomings.<br />
It’s fun to develop your own little dialect and completely forget about it ten minutes later. It’s infamous as a write-only language because no one can read its arcane symbols or figure out what the hell anyone else is doing.</p>

<p>But now a ton of new code is getting generated this way. The harder you vibe, the less you read the code. I have<br />
entire applications where I haven’t yet <em>opened</em> all of the files. Code gets written, not read. Vibe-coding is a continuum,<br />
and I expect the ratio of vibe-to-artisanal code to increase as the LLMs get better and as more people get comfortable with<br />
the tools. Everything is Perl now.</p>

        </div>
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Sloppy Syntax]]></title>
      <description><![CDATA[A personal collection of totally radioactive syntax often generated by LLMs.]]></description>
      <pubDate>Sat, 22 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://johnsaigle.com/posts/sloppy-syntax</link>
      <guid isPermaLink="true">https://johnsaigle.com/posts/sloppy-syntax</guid>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 680px;">
          <p>A personal collection of totally radioactive syntax often generated by LLMs.</p>

<h2 id="stale-rhetorical-devices">Stale Rhetorical Devices</h2>
<ul>
  <li>Contrastive negation, i.e. any variation on: <em>“It’s not X – it’s Y”</em>.</li>
  <li>Unnecessary meta-commentary (<em>“So here’s the question,” “here’s the interesting part”</em>)</li>
  <li><em>“The math is simple”</em></li>
  <li>Developing ideas in essays via <a href="https://en.wikipedia.org/wiki/Anaphora_(rhetoric)">anaphora</a></li>
  <li>Obvious alliteration</li>
</ul>

<h2 id="seo-and-algorithm-gamification">SEO and Algorithm Gamification</h2>
<ul>
  <li>New markdown headings that interject prose that already flows well</li>
  <li>Posts consisting of individual sentences separated by more than one new line</li>
  <li>Emojis as punctuation</li>
  <li>Emojis in markdown headings</li>
  <li>Ending pieces with “The Bottom Line”; superfluous summary pontification</li>
  <li>Calls-to-action outside of a marketing context</li>
</ul>

<h2 id="sloppy-syntax">Sloppy Syntax</h2>
<ul>
  <li>Em dashes (other than rare occurrences)</li>
  <li>Bolded text</li>
</ul>

<h2 id="phrases-id-never-use">Phrases I’d never use</h2>
<ul>
  <li>AI assistant</li>
  <li>pair programmer</li>
</ul>

        </div>
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Security is PvPvE]]></title>
      <description><![CDATA[Security work isn&#39;t just about outsmarting attackers. You&#39;re also fighting the computers themselves.]]></description>
      <pubDate>Mon, 17 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://johnsaigle.com/posts/security-is-pvpve</link>
      <guid isPermaLink="true">https://johnsaigle.com/posts/security-is-pvpve</guid>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 680px;">
          <p>Like much of the gaming world lately, I’ve been playing a lot of Arc Raiders. The game is <br />
described as PvPvE: Player vs. Player vs. Environment. Players explore a shared <br />
post-apocalyptic landscape to scavenge rare materials. They can cooperate or attack <br />
each other, stealing whatever loot their opponents have collected. But patrolling these <br />
abandoned space ports and half-buried cities are killer robots, who attack everyone <br />
regardless of alliance or intent.</p>

<p>The field of computer security works the same way, but most people think of it as <br />
exclusively Player vs. Player: will the security engineer outsmart the hacker, or vice-versa?<br />
We define our philosophies as white hat or black hat and describe our activity as<br />
red or blue teaming.</p>

<p>But security work also has a PvE element. When securing systems, we struggle not just against <br />
agents with opposed intentions, but against the environment itself. The computers are trying to <br />
kill us. We don’t need to ascribe intent here; it’s observable from their behavior.</p>

<p>This makes sense when you realize that computer security is, for the most part, a subset of software quality. It shouldn’t be <br />
possible to hack software of the highest quality. (Social engineering and wrench attacks <br />
sidestep the software entirely, so they don’t count.)</p>

<p>Problems like denial-of-service, technical debt, or poorly-designed software are indistinguishable <br />
from “real” security bugs. Unreliable, calcified systems are extremely difficult to secure because <br />
it’s not always possible to test their behavior and prove their security properties.</p>

<p>The history of computing is full of these environmental failures. Early networks weren’t encrypted <br />
because they assumed trusted users on trusted infrastructure. Multi-user operating systems shipped <br />
without password protection because designers couldn’t imagine adversarial access. The environment <br />
changed, but the systems couldn’t adapt without heavy intervention.</p>

<p>The same pattern plays out in modern systems. Flashloan attacks in Solidity weren’t possible until <br />
DeFi protocols composed in ways the original developers never anticipated. Invariant-based design <br />
could have prevented this—not by predicting flashloans specifically, but by encoding constraints <br />
that hold regardless of the execution environment.</p>

<p>Why do these environmental failures keep happening? Because the environment actively fights you. <br />
Brittle code, legacy systems, poor abstractions, missing tests—a codebase with little in the way of test coverage <br />
or proactive assertions of invariants can’t be refactored safely. A system built on fragile assumptions breaks under novel inputs. A service with ten dependency layers fails in ways no one can predict. These aren’t just obstacles <br />
to security work. They <em>are</em> security problems.</p>

<p>Yet this isn’t how most people think about security, because environmental issues are less obvious <br />
and sexy than a major compromise. It also doesn’t fit well into the offensive security mindset oriented <br />
toward time-boxed evaluations: pentests, code audits, contests, and bug bounties. These <br />
problems are more like a disease that slowly spreads and only becomes visible when it’s <br />
already lethal.</p>

<p>The PvPvE frame lacks the narrative drama of hacker-versus-hacker showdowns, but it better <br />
explains the actual work. You’re not just fighting attackers. You’re fighting the computers themselves.</p>

<p>And if you ignore the environment, you’re already halfway to losing the game.</p>

        </div>
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Bernoulli Coding]]></title>
      <description><![CDATA[The middle ground in software development is dead. I now write code in just two modes: fortress-grade security or complete disposable chaos.]]></description>
      <pubDate>Tue, 02 Sep 2025 00:00:00 +0000</pubDate>
      <link>https://johnsaigle.com/posts/bernoulli-coding</link>
      <guid isPermaLink="true">https://johnsaigle.com/posts/bernoulli-coding</guid>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 680px;">
          <p>The middle ground in software development is dead.</p>

<p>After a decade of building systems, reviewing code, and hunting vulnerabilities, I’ve found myself writing software in exactly two modes. There’s the fortress: ultra-secure, thoroughly tested, peer-reviewed, and bulletproof. Then there’s the sketch: completely disposable, built for speed, meant to break.</p>

<p>This approach eliminates the exhausting middle tier where projects hover in development purgatory. Those half-robust side projects that might work, might scale, might be worth the CI setup and security audit. The ones that consume mental energy without clarity of purpose.</p>

<h2 id="the-two-extremes">The Two Extremes</h2>

<p><strong>Fortress Mode</strong> applies to anything handling sensitive data or critical workflows. These projects get the full treatment: linters cranked to maximum sensitivity, comprehensive test suites, security reviews, and deployment pipelines. Every dependency is audited. Every function is documented.</p>

<p>This rigor exists because most software frustrations stem from cutting corners. Web apps that constantly break, services that leak data, systems that crumble under load. When something matters, it deserves relentless attention.</p>

<p>This is how I code for work, or in any situation where someone is counting on me to do something properly.</p>

<p><strong>Sketch Mode</strong> covers scripts, proof-of-concepts, and experimental tools. Here, I deliberately turn off the security reviewer part of my brain. No Rust type system enforcement, no memory safety concerns, no linting. The goal is pure velocity: get the idea out of your head and into working form.</p>

<p>These tools exist to answer a simple question: is this concept worth pursuing? Most aren’t. They’re incomplete, abandoned after the initial burst of curiosity, or require significant rework to become truly useful. Starting with heavy tooling would kill the experimentation entirely.</p>

<p>This goes all the way down to language choice. I won’t vibe-code Rust, and I won’t try to write secure, reliable software<br />
in an interpreted language. Getting clear on the actual use of the software allows one to play to the respective<br />
strengths of the tools used to build it.</p>

<h2 id="the-excluded-middle">The Excluded Middle</h2>

<p>Even though both of the above approaches are extreme in their own ways, it’s actually the mid-tier projects that I find<br />
most exhausting to work with. Fortress-style should be lovely to read and should work every time. Sketch mode code<br />
should never, ever be read and is almost something close to an aesthetic, playful experience than to something like<br />
engineering.</p>

<p>Most of the software out there isn’t truly fortress-grade or unapologetically sketchy. Instead, it’s fragile code pretending to be robust—a proof-of-concept stretched far past its expiration date. Most software should be thought of as<br />
slop until proven otherwise, just because doing things right ends up being beyond the interest, ability, or resources<br />
of most people and organizations. Maybe all of the LLM-generated code will make more people realize and/or admit this.</p>

<h2 id="putting-out-slop">Putting Out Slop</h2>

<p>I treat the sketch projects like hazardous materials, clearly labeled from the start. The README gets a prominent warning: “This is vibe-coded software. Use at your own risk.” No security guarantees or reliability promises, just an idea I had that someone<br />
else might find interesting too.</p>

<p>More broadly, I’m sympathetic to the idea of <a href="https://www.youtube.com/watch?v=vagyIcmIGOQ&amp;t=20195">FOSS as a gift economy</a><br />
rather than as an open-ended service contract. If I was stuck in that mindset, I wouldn’t be able to make and share these<br />
projects knowing that they almost certainly have ridiculous problems as well as security issues. It’s not even so<br />
much that I’m genuinely worried that someone will get hacked because of something that I wrote, but more that<br />
it takes more mental energy to write code securely, and that’s not the point of these projects.</p>

<p>Keeping these two mindsets totally separate has felt deeply liberating. I hope others do the same, so that we can<br />
all be a little bit more clear about what we’re getting ourselves into when we run something off of GitHub.</p>

        </div>
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Slopsquatting detection added to Scary Strings]]></title>
      <description><![CDATA[Preventing AIs from installing sketchy packages]]></description>
      <pubDate>Sat, 30 Aug 2025 00:00:00 +0000</pubDate>
      <link>https://johnsaigle.com/posts/slopsquatting-detection-added-to-scary-strings</link>
      <guid isPermaLink="true">https://johnsaigle.com/posts/slopsquatting-detection-added-to-scary-strings</guid>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 680px;">
          <p>Vibe-coding sessions can go awry in many ways. One way is for the agent to offer to install a weird package. It will frequently pull an obscure GitHub repo over an official SDK. AI training data is biased toward whatever packages were popular or well-documented during training, not necessarily what’s safe or well-maintained.</p>

<p>This happened to me recently: I was trying to connect Go with OpenAI when the agent suggested installing an unofficial implementation over OpenAI’s own SDK. Nothing obviously wrong with that particular package, but it got me thinking about how this could be prevented. We’re automating ourselves toward new vulnerabilities. As we allow these tools to become more autonomous, we need defenses that work automatically too.</p>

<p>So I added a new <a href="https://github.com/johnsaigle/scary-strings/tree/main/wordlists/llm/slopsquat"><code>slopsquat</code> directory</a> to <a href="https://github.com/johnsaigle/scary-strings/">Scary Strings</a> that tracks risky packages.</p>

<p>My immediate motivation is to use it to track and prevent strange packages that AI tools suggest in the wild,<br />
though of course it is also simply a collection of weird libraries that go against sensible defaults.</p>

<p>Developers can then wire this list into their package managers (like <a href="https://golangci-lint.run/docs/linters/configuration/#depguard"><code>depguard</code> in Go</a>) to automatically reject these suggestions before they make it into production.</p>

<p>Using this list to block packages in CI protects against both human mistakes and AI agents running wild. Whether you’re a dev who is too busy to investigate every package, or an autonomous agent in full YOLO mode, the safety net catches both.</p>

        </div>
      ]]></content:encoded>
    </item>
  </channel>
</rss>
