<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>The Hack Job Handbook</title>
    <description>A blog about hacking, programming, and other tech stuff.</description>
    <link>https://nihilok.github.io</link>
    <atom:link href="https://nihilok.github.io/feed.xml" rel="self" type="application/rss+xml" />
    <author>
      <name>nihilok</name>
      <email>mike@jarv.dev</email>
      <uri>https://nihilok.github.io</uri>
    </author>
    
      <item>
        <title>Your Agent Doesn&apos;t Need a README</title>
        <description>&lt;p&gt;Your AI agent is reading your README right now. It’s on paragraph three of the “Getting Started” section, trying to figure out whether &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm run dev&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make dev&lt;/code&gt; is the current way to start the project. It will get there eventually. That’s the problem.&lt;/p&gt;

&lt;p&gt;We write READMEs for humans. Structured headings, prose explanations, code blocks with context. A human reads “To run the tests, use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo test --workspace&lt;/code&gt;” and knows what to do. But an AI agent reads the same sentence and has to:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Find the README&lt;/li&gt;
  &lt;li&gt;Parse the natural language&lt;/li&gt;
  &lt;li&gt;Extract the command from the surrounding prose&lt;/li&gt;
  &lt;li&gt;Hope the README is up to date&lt;/li&gt;
  &lt;li&gt;Hope there aren’t flags mentioned three paragraphs later that are also required&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It works. Eventually. But it’s the wrong interface for the job.&lt;/p&gt;

&lt;h2 id=&quot;structured-beats-unstructured&quot;&gt;Structured beats unstructured&lt;/h2&gt;

&lt;p&gt;READMEs are documentation. They’re great at explaining &lt;em&gt;why&lt;/em&gt;. They’re terrible at telling a machine &lt;em&gt;what&lt;/em&gt;, because the machine has to do natural language parsing on a document designed for humans, and extract structured information from an unstructured format.&lt;/p&gt;

&lt;p&gt;What agents actually need is a schema: a tool name, a description, typed parameters, and defaults. That’s what &lt;a href=&quot;https://modelcontextprotocol.io&quot;&gt;MCP&lt;/a&gt; provides.&lt;/p&gt;

&lt;p&gt;A &lt;a href=&quot;https://runtool.dev/docs&quot;&gt;Runfile&lt;/a&gt; gives your project’s tools exactly that schema:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# @desc Run the test suite&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# @arg filter Optional test name filter&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;filter &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    cargo &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--workspace&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-fail-fast&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$filter&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The agent doesn’t read this file. It receives a structured tool definition over MCP: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt; is a tool, it takes an optional string parameter called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt;, and here’s what it does. No parsing. No ambiguity.&lt;/p&gt;

&lt;h2 id=&quot;what-the-agent-sees&quot;&gt;What the agent sees&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;From a README:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A blob of Markdown. It has to figure out which parts are commands, which parts are commentary, and which parts are outdated. It might get it right. It might try the example from the “Legacy Setup” section that nobody deleted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From an MCP tool registry:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Run the test suite&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;parameters&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Optional test name filter&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;No ambiguity. No staleness risk. The schema &lt;em&gt;is&lt;/em&gt; the interface.&lt;/p&gt;

&lt;h2 id=&quot;what-the-agent-doesnt-see&quot;&gt;What the agent doesn’t see&lt;/h2&gt;

&lt;p&gt;Here’s the other thing: when the agent reads your README to discover commands, it also reads everything else in there. Internal URLs, architecture decisions, deployment details, service names. All of it goes into the context window.&lt;/p&gt;

&lt;p&gt;An MCP tool registry exposes &lt;em&gt;only&lt;/em&gt; the tool interface: name, description, parameters. The implementation stays behind the wall. Your agent gets powerful, well-defined tools without getting a map of your internals.&lt;/p&gt;

&lt;h2 id=&quot;deterministic-beats-probabilistic&quot;&gt;Deterministic beats probabilistic&lt;/h2&gt;

&lt;p&gt;There’s a deeper point here. When an agent reads a README and extracts a command, the result is probabilistic. It &lt;em&gt;probably&lt;/em&gt; gets the right command. It &lt;em&gt;probably&lt;/em&gt; passes the right flags. But “probably” compounds badly across a session. Each probably-correct step increases the chance that one of them isn’t.&lt;/p&gt;

&lt;p&gt;A Runfile is deterministic. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run test&lt;/code&gt; runs the test suite, with the right flags, every time. The agent doesn’t interpret instructions, it calls a tool. There’s no gap between what you intended and what executes.&lt;/p&gt;

&lt;p&gt;This matters even more when you combine it with Claude Code’s &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/skills&quot;&gt;skills&lt;/a&gt;. You can write a skill that says “before committing, always run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ci&lt;/code&gt;” — and because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ci&lt;/code&gt; is a deterministic MCP tool, not a natural language instruction, the behaviour is predictable and auditable. Skills tell the agent &lt;em&gt;when&lt;/em&gt; to act. The Runfile tells it &lt;em&gt;what&lt;/em&gt; to do. Keeping those concerns separate makes both more reliable.&lt;/p&gt;

&lt;h2 id=&quot;the-readme-is-still-useful&quot;&gt;The README is still useful&lt;/h2&gt;

&lt;p&gt;This isn’t an argument against READMEs. Write them for your human teammates. Explain the &lt;em&gt;why&lt;/em&gt;, the architecture, the gotchas.&lt;/p&gt;

&lt;p&gt;You can even document your Runfile commands in the README. They’re just as useful on the command line as they are via MCP: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run test&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run deploy staging&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run ci&lt;/code&gt;. The README explains when and why to use them. The Runfile is the executable source of truth.&lt;/p&gt;

&lt;p&gt;But stop expecting your agent to use the README as an API. Give it a real one.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;nihilok/tap/runtool
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/nihilok/run&quot;&gt;GitHub&lt;/a&gt; · &lt;a href=&quot;https://runtool.dev/docs&quot;&gt;Docs&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Sun, 08 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://nihilok.github.io//your-agent-doesn-t-need-a-readme</link>
        <link href="https://nihilok.github.io/your-agent-doesn-t-need-a-readme"/>
        <guid isPermaLink="true">https://nihilok.github.io/your-agent-doesn-t-need-a-readme</guid>
      </item>
    
      <item>
        <title>How I Gave Claude Code a Deterministic Toolbox (and Stopped It Guessing How to Run My Project)</title>
        <description>&lt;p&gt;If you’ve spent any real time pairing with Claude Code, you’ve seen the dance.&lt;/p&gt;

&lt;p&gt;You ask it to run the tests. It reads your README. Maybe it finds a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Makefile&lt;/code&gt;, maybe a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt;, maybe a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scripts/&lt;/code&gt; directory. It tries &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make test&lt;/code&gt;. That fails. It tries &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt;. Wrong project. It &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cat&lt;/code&gt;s your Makefile, parses the targets, picks one, runs it with the wrong arguments. Three tool calls and 2,000 tokens later, it’s running the right command — until next session, when it’s forgotten everything and does the dance again.&lt;/p&gt;

&lt;p&gt;This is the discovery tax. Every Claude Code session pays it, and it compounds: each failed attempt burns context window, each retry eats tokens, and the agent’s confidence degrades as it accumulates error messages. On a long session where you’re already bumping up against context limits, those wasted tokens matter.&lt;/p&gt;

&lt;p&gt;I built a tool called &lt;a href=&quot;https://runtool.dev&quot;&gt;run&lt;/a&gt; that fixes this. Here’s how.&lt;/p&gt;

&lt;h2 id=&quot;the-problem-isnt-intelligence-its-discovery&quot;&gt;The problem isn’t intelligence, it’s discovery&lt;/h2&gt;

&lt;p&gt;Claude Code is remarkably good at executing tasks once it knows what to do. The bottleneck is the gap between “I need to run the tests” and “the command is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo test --workspace --no-fail-fast&lt;/code&gt;”. Bridging that gap currently requires the agent to:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Read files&lt;/strong&gt; to discover what’s available (README, Makefile, package.json, scripts/)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Infer intent&lt;/strong&gt; from naming conventions and comments&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Guess at arguments&lt;/strong&gt; — is it &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make deploy ENV=staging&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make deploy-staging&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./scripts/deploy.sh staging&lt;/code&gt;?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fail and retry&lt;/strong&gt; when the guess is wrong&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Forget everything&lt;/strong&gt; next session and repeat from step 1&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of these steps consumes tool calls and context. And because there’s no structured metadata — just files full of text — the agent is doing natural language parsing of shell scripts, which is exactly the kind of task where LLMs are most likely to make subtle mistakes.&lt;/p&gt;

&lt;h2 id=&quot;what-if-the-agent-already-knew&quot;&gt;What if the agent already knew?&lt;/h2&gt;

&lt;p&gt;MCP (Model Context Protocol) lets you expose tools to AI agents with structured schemas — name, description, typed parameters, defaults. The agent doesn’t discover tools by reading files; it discovers them through a protocol designed for exactly this purpose.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://runtool.dev&quot;&gt;RunTool&lt;/a&gt; is a task runner with a built-in MCP server. You define your project’s tasks in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Runfile&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# @desc Run the full test suite
# @arg filter Optional test name filter
test(filter = &quot;&quot;) {
    cargo test --workspace --no-fail-fast $filter
}

# @desc Deploy to the specified environment
# @arg environment Target environment (staging|prod)
deploy(environment) {
    ./scripts/deploy.sh $environment
}

# @desc Resize an image using Python
# @arg file Path to the image file
# @arg width Target width in pixels
# @arg height Target height in pixels
resize(file, width: int, height: int) {
    #!/usr/bin/env python
    from PIL import Image
    img = Image.open(file)
    img.resize((width, height)).save(file)
    print(f&quot;Resized to {width}x{height}&quot;)
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt; as an MCP server in your Claude Code config:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;runtool&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;run&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;args&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;--serve-mcp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now when Claude Code starts a session, it immediately sees a tool registry: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt; takes an optional string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt;; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy&lt;/code&gt; takes a required string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;environment&lt;/code&gt;; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resize&lt;/code&gt; takes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file&lt;/code&gt; string plus &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;width&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;height&lt;/code&gt; integers. No file reading. No guessing. No retry loops. Zero discovery tax.&lt;/p&gt;

&lt;h2 id=&quot;before-and-after&quot;&gt;Before and after&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before (no MCP, typical Claude Code session):&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Human: run the tests

Claude Code: I&apos;ll look for how to run tests in this project.
  [tool] cat README.md                           → 800 tokens
  [tool] ls scripts/                              → 50 tokens
  [tool] cat Makefile                             → 600 tokens
  [tool] make test                                → fails (wrong target name)
  [tool] make tests                               → fails (missing argument)
  [tool] make tests FILTER=&quot;&quot;                     → success

  Total: 6 tool calls, ~2,500 tokens of context consumed
  Next session: starts from scratch
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;After (RunTool MCP):&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Human: run the tests

Claude Code: I&apos;ll run the test suite.
  [tool] mcp:runtool test                          → success

  Total: 1 tool call, ~100 tokens of context consumed
  Next session: same tool registry, same result
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s not a marginal improvement. It’s the difference between an agent that fumbles through your project and one that operates like a team member who already knows the codebase.&lt;/p&gt;

&lt;h2 id=&quot;auto-truncation-solving-the-output-problem&quot;&gt;Auto-truncation: solving the output problem&lt;/h2&gt;

&lt;p&gt;There’s a second problem that anyone using MCP tools with Claude Code or Codex has run into: output truncation. Claude Code and Codex both impose limits on tool output — and when your test suite dumps 500 lines of results, the agent gets a chopped-up view with the middle missing, which is often exactly where the useful information is.&lt;/p&gt;

&lt;p&gt;RunTool handles this at the MCP server level. By default, tool output is capped at approximately 300 tokens (~1KB) — enough for the agent to see whether something passed or failed and get the key details, without blowing up the context window. Critically, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt; keeps the &lt;em&gt;tail&lt;/em&gt; of the output, not the head. That’s where the useful information almost always is: the test results summary, the final error message, the exit status. When output exceeds the limit, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt; truncates from the top and saves the full output to a file. The agent gets the actionable ending plus a file path it can read selectively if it needs the full details. The threshold is configurable in the environment, so you can tune it up or down to match your context budget.&lt;/p&gt;

&lt;p&gt;Compare this to what happens without it: Claude Code and Codex both impose their own truncation on tool output — Codex chops at 256 lines or 10KB using a head+tail strategy that drops the middle, which is often exactly where the useful information is. RunTool’s approach is smarter because it happens at the source, before the agent’s own limits kick in, it prioritises the part of the output you actually care about, and the full output is always recoverable.&lt;/p&gt;

&lt;h2 id=&quot;the-security-argument-sandboxing-through-metadata&quot;&gt;The security argument: sandboxing through metadata&lt;/h2&gt;

&lt;p&gt;Here’s something that’s easy to overlook but has real implications for how you think about agent access.&lt;/p&gt;

&lt;p&gt;When Claude Code reads your Makefile or scripts to discover tasks, it sees &lt;em&gt;everything&lt;/em&gt;: implementation details, file paths, secrets files being sourced, internal service URLs, database connection strings referenced in scripts. All of that goes into the context window.&lt;/p&gt;

&lt;p&gt;RunTool’s MCP server exposes &lt;em&gt;only&lt;/em&gt; the annotations — function name, description, argument signatures. The implementation stays hidden. The agent knows that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy&lt;/code&gt; takes an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;environment&lt;/code&gt; argument of type string; it doesn’t know that internally it sources &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.secrets/prod.env&lt;/code&gt; and shells out to an internal deployment service at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy.internal.corp:8443&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is sandboxing through metadata. You’re giving the agent powerful, well-defined tools without giving it a map of your internals. If the agent wants to read the Runfile itself it can (it’s just a file), but that’s an explicit read operation that you could restrict — not something the MCP server hands over automatically.&lt;/p&gt;

&lt;p&gt;For teams that are cautious about what their AI agents can see — and you should be — this is a meaningful property.&lt;/p&gt;

&lt;h2 id=&quot;the-deterministic-layer-encoding-skills-in-your-runfile&quot;&gt;The deterministic layer: encoding skills in your Runfile&lt;/h2&gt;

&lt;p&gt;Claude Code has a concept of “skills” — instructions stored in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/&lt;/code&gt; that tell the agent how to handle specific tasks. Skills are useful, but they’re prompts: natural language instructions that the agent interprets probabilistically. They can drift, be misinterpreted, or interact unpredictably with other context.&lt;/p&gt;

&lt;p&gt;A Runfile is a deterministic skill layer. When you define:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# @desc Run linting with auto-fix
lint() cargo clippy --fix --allow-dirty

# @desc Format all source files
fmt() cargo fmt --all

# @desc Run the full CI pipeline locally
ci() {
    run lint
    run fmt
    run test
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There’s no interpretation involved. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ci&lt;/code&gt; runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lint&lt;/code&gt;, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fmt&lt;/code&gt;, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt;, in that order, every time. The agent doesn’t need to figure out your CI pipeline from scattered config files — it has a single, composable tool that does exactly what you’ve defined.&lt;/p&gt;

&lt;p&gt;This is particularly powerful when combined with skills. You can write a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/&lt;/code&gt; skill that says “before committing, always run the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ci&lt;/code&gt; tool” — and because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ci&lt;/code&gt; is a deterministic MCP tool rather than a set of natural language instructions, the behaviour is predictable and auditable.&lt;/p&gt;

&lt;p&gt;Skills tell the agent &lt;em&gt;when&lt;/em&gt; to act. The Runfile tells it &lt;em&gt;what&lt;/em&gt; to do. Keeping those concerns separate makes both more reliable.&lt;/p&gt;

&lt;h2 id=&quot;getting-started&quot;&gt;Getting started&lt;/h2&gt;

&lt;p&gt;Install &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;nihilok/tap/runtool
&lt;span class=&quot;c&quot;&gt;# or&lt;/span&gt;
cargo &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;run
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Runfile&lt;/code&gt; in your project root with your common tasks. Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@desc&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@arg&lt;/code&gt; annotations for anything you want the agent to discover. Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt; as an MCP server in your Claude Code config.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://runtool.dev/docs&quot;&gt;documentation&lt;/a&gt; covers the full syntax, including polyglot scripting (Python, Node, Ruby, PowerShell in the same file), cross-platform &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@os&lt;/code&gt; variants, and command composition.&lt;/p&gt;

&lt;p&gt;The code is on &lt;a href=&quot;https://github.com/nihilok/run&quot;&gt;GitHub&lt;/a&gt;. If you’re working with Claude Code daily and you’re tired of watching it guess how to run your project, give it a try.&lt;/p&gt;
</description>
        <pubDate>Sat, 07 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://nihilok.github.io//deterministic-toolbox-for-claude-code</link>
        <link href="https://nihilok.github.io/deterministic-toolbox-for-claude-code"/>
        <guid isPermaLink="true">https://nihilok.github.io/deterministic-toolbox-for-claude-code</guid>
      </item>
    
      <item>
        <title>Integration Test Isolation in a Next.js App with Drizzle ORM</title>
        <description>&lt;p&gt;&lt;img src=&quot;./assets/testing-strategies.png&quot; alt=&quot;test-isolation&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;1-introduction-the-complexity-of-modern-integration-testing&quot;&gt;1. Introduction: The Complexity of Modern Integration Testing&lt;/h2&gt;

&lt;p&gt;The evolution of web application architecture has precipitously increased the complexity of ensuring software quality. In the era of the monolithic, server-rendered application—typified by frameworks such as Ruby on Rails or early ASP.NET—testing strategies were relatively straightforward. The application ran in a single process, connected to a single relational database, and served HTML directly. Integration testing in such an environment was a solved problem: wrap the test in a database transaction, execute the logic, and roll back the transaction at the end of the test. This approach, often referred to as the “transactional rollback” strategy, provided a clean slate for every test case, ensuring isolation and determinism with minimal overhead.&lt;/p&gt;

&lt;p&gt;However, the modern full-stack landscape, dominated by React meta-frameworks like Next.js and diverse data-access layers such as Drizzle ORM, has fundamentally altered this equation. The shift towards serverless compute models, the introduction of React Server Components (RSC), and the decoupling of the frontend from the backend logic via mechanisms like Server Actions have introduced new boundaries and constraints. In this environment, the database is no longer just a passive store of state; it is a shared mutable resource accessed by highly concurrent, asynchronous processes that may not share the same memory space or execution context as the test runner.&lt;/p&gt;

&lt;p&gt;This report provides an exhaustive analysis of the strategies available for achieving rigorous integration test isolation in a Next.js application using Drizzle ORM. It explores the theoretical underpinnings of database isolation, the specific architectural challenges posed by the Next.js App Router, and the practical implementation of four distinct testing methodologies: Transactional Rollbacks, Database Truncation, Containerised Isolation (Testcontainers), and In-Memory WebAssembly Databases (PGlite). By examining the mechanisms, performance characteristics, and trade-offs of each approach, this document aims to equip engineering teams with the knowledge required to architect robust, scalable, and non-flaky test suites for mission-critical applications.&lt;/p&gt;

&lt;h3 id=&quot;11-the-imperative-of-isolation&quot;&gt;1.1 The Imperative of Isolation&lt;/h3&gt;

&lt;p&gt;Integration testing sits at the precarious intersection of the testing pyramid. Unlike unit tests, which verify discrete logic in isolation by mocking external dependencies, integration tests must validate the interaction between the application code and its infrastructure—specifically, the database. The fidelity of these tests is paramount. If a test mocks the database driver, it ceases to be an integration test; it merely tests the developer’s assumptions about how the database should behave, rather than how it actually behaves.&lt;/p&gt;

&lt;p&gt;The central challenge in integration testing is &lt;strong&gt;Shared Mutable State&lt;/strong&gt;. When a test suite runs, multiple test cases execute against the database. If these tests are not perfectly isolated, they interfere with one another, leading to a class of failures known as “flaky tests.” Flakiness manifests in several ways:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Data Pollution&lt;/strong&gt;: One test inserts a record (e.g., a user with email &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test@example.com&lt;/code&gt;) that persists after the test finishes. A subsequent test, expecting an empty user table or attempting to create a user with the same unique email, fails.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Phantom Reads&lt;/strong&gt;: In a concurrent test environment, a query in Test A might inadvertently retrieve data inserted by Test B, leading to assertion failures that are impossible to reproduce when running the test in isolation.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Race Conditions&lt;/strong&gt;: Tests that modify global configuration or shared singleton resources can create non-deterministic outcomes based on the precise timing of execution by the CPU scheduler.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To mitigate these risks, the testing environment must guarantee that every test runs in a pristine environment. The database state at the beginning of a test must be known, deterministic, and unaffected by any previous or concurrent test execution.&lt;/p&gt;

&lt;h3 id=&quot;12-the-nextjs-effect-architectural-constraints&quot;&gt;1.2 The “Next.js Effect”: Architectural Constraints&lt;/h3&gt;

&lt;p&gt;Next.js, particularly with its App Router architecture introduced in version 13, imposes specific constraints that complicate traditional isolation strategies.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Server Actions as Remote Procedure Calls (RPC)&lt;/strong&gt;: Server Actions in Next.js allows functions to be executed on the server, triggered directly from client-side components. From a testing perspective, these are not simple function calls; they are asynchronous operations that often run in a separate context. Testing them requires an environment that can simulate the Next.js server runtime, including access to environment variables, headers, and cookies.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;The Singleton Pattern &amp;amp; Module Caching&lt;/strong&gt;: In a typical Next.js application, the database client (Drizzle instance) is instantiated as a global singleton. This is necessary to prevent connection exhaustion during development (hot reloading) and to manage connection pooling in production. However, this global singleton makes Dependency Injection (DI)—a primary technique for test isolation—significantly more difficult. If a Server Action imports the database client directly from a static module path (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import { db } from &apos;@/db&apos;&lt;/code&gt;), the test runner must intervene at the module loading level to inject a test-specific database instance.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Server-Only Boundaries&lt;/strong&gt;: Next.js enforces strict boundaries between server and client code using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server-only&lt;/code&gt; package. Integration tests that import server-side logic must execute in a Node.js-compatible environment (like vitest with a node environment) rather than a browser-like environment (like jsdom), or they will trigger build-time errors. This constrains the choice of test runners and mocking strategies.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;13-drizzle-orm-a-typescript-first-approach&quot;&gt;1.3 Drizzle ORM: A TypeScript-First Approach&lt;/h3&gt;

&lt;p&gt;Drizzle ORM represents a modern approach to database interaction, favoring type safety and SQL-like syntax over the heavy abstraction layers of traditional ORMs. It separates the query builder (the TypeScript API) from the driver (the mechanism that talks to the database). This separation is crucial for testing because it allows the underlying driver to be swapped—for example, replacing a network-based Postgres client with an in-memory PGlite client—without changing the application logic. However, Drizzle’s reliance on specific driver features (like prepared statements or specific transaction APIs) means that the test environment must closely mirror the production environment’s capabilities to avoid false positives.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;2-theoretical-framework-acid-properties-and-test-concurrency&quot;&gt;2. Theoretical Framework: ACID Properties and Test Concurrency&lt;/h2&gt;

&lt;p&gt;To understand why integration test isolation is difficult, one must delve into the fundamental properties of relational databases: ACID (Atomicity, Consistency, Isolation, Durability). Integration testing strategies essentially manipulate these properties to achieve their goals.&lt;/p&gt;

&lt;h3 id=&quot;21-atomicity-and-test-boundaries&quot;&gt;2.1 Atomicity and Test Boundaries&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Atomicity&lt;/strong&gt; guarantees that a transaction is treated as a single “unit of work,” which either completely succeeds or completely fails. The Transactional Rollback strategy leverages this property. By wrapping a test case in a transaction, the test runner ensures that all database writes are provisional. When the test concludes, the runner issues a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ROLLBACK&lt;/code&gt; command, which atomically undoes all changes. This relies on the database’s Write-Ahead Log (WAL) to revert the state efficiently, without the overhead of deleting records from the disk.&lt;/p&gt;

&lt;p&gt;However, Atomicity interacts complexly with modern application logic. If the application code under test itself uses transactions (e.g., a signup flow that creates a user and an organisation in a single transaction), the test runner must support &lt;strong&gt;Nested Transactions&lt;/strong&gt;. In PostgreSQL, true nested transactions do not exist; they are simulated using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SAVEPOINT&lt;/code&gt;. Drizzle ORM supports this via its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tx&lt;/code&gt; API, allowing a test to start a transaction (Level 1) and the application to start a nested transaction (Level 2). A rollback of Level 2 reverts to the savepoint, while a rollback of Level 1 reverts everything.&lt;/p&gt;

&lt;h3 id=&quot;22-database-isolation-levels&quot;&gt;2.2 Database Isolation Levels&lt;/h3&gt;

&lt;p&gt;The “I” in ACID refers to &lt;strong&gt;Isolation&lt;/strong&gt;—the degree to which a transaction is protected from the effects of other concurrent transactions. PostgreSQL offers several isolation levels:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Read Committed (Default)&lt;/strong&gt;: A query sees only data committed before the query began.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Repeatable Read&lt;/strong&gt;: A query sees a snapshot of the database as of the start of the transaction.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Serialisable&lt;/strong&gt;: The strictest level, ensuring that the result of concurrent transactions is the same as if they had executed serially.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For integration tests running in parallel against a shared database, Read Committed is often insufficient. If Test A (running in a transaction) inserts a record, and Test B (also in a transaction) runs a query that counts records, Test B generally won’t see Test A’s uncommitted data. However, if Test A commits (which might happen if the application logic forces a commit), Test B’s state is polluted. This creates a “race condition” where tests pass or fail depending on which one finishes first. Achieving true isolation usually requires ensuring that tests running in parallel never share the same database instance, or use strict row-level locking which degrades performance.&lt;/p&gt;

&lt;h3 id=&quot;23-the-throughput-vs-latency-trade-off-in-testing&quot;&gt;2.3 The Throughput vs. Latency Trade-off in Testing&lt;/h3&gt;

&lt;p&gt;Architecting a test suite involves a fundamental trade-off between &lt;strong&gt;Latency&lt;/strong&gt; (how fast a single test runs) and &lt;strong&gt;Throughput&lt;/strong&gt; (how fast the entire suite runs).&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Serial Execution&lt;/strong&gt;: Running tests one by one (latency focused) is the easiest way to ensure isolation. You can use a single database and truncate it between tests. However, as the suite grows to thousands of tests, the total execution time (throughput) becomes unacceptable, leading to slow CI pipelines.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Parallel Execution&lt;/strong&gt;: Running tests concurrently (throughput focused) drastically reduces total time but requires advanced isolation strategies. Each parallel worker needs its own isolated environment (e.g., its own database schema or container) to prevent collisions. This increases the resource cost (CPU/RAM) of the test runner.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following sections analyze four strategies that navigate this trade-off space differently.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;3-strategy-i-transactional-rollbacks-the-rails-pattern&quot;&gt;3. Strategy I: Transactional Rollbacks (The “Rails” Pattern)&lt;/h2&gt;

&lt;p&gt;The Transactional Rollback strategy is often considered the “gold standard” in frameworks like Ruby on Rails and Django. It promises the best of both worlds: speed (no need to recreate schemas) and isolation (changes are never committed).&lt;/p&gt;

&lt;h3 id=&quot;31-mechanism-of-action&quot;&gt;3.1 Mechanism of Action&lt;/h3&gt;

&lt;p&gt;In this model, the test runner performs the following sequence for each test case:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Connect&lt;/strong&gt;: Establish a connection to the database.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Begin&lt;/strong&gt;: Execute &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BEGIN&lt;/code&gt; to start a transaction.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Run&lt;/strong&gt;: Execute the test logic. The application performs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSERT&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt; operations. These changes are visible to the connection but not to the outside world.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Rollback&lt;/strong&gt;: Execute &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ROLLBACK&lt;/code&gt; in the teardown or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;afterEach&lt;/code&gt; hook. The database discards the changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach is highly efficient because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ROLLBACK&lt;/code&gt; is an inexpensive operation for the database engine compared to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUNCATE&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt;, which require scanning tables and updating indexes.&lt;/p&gt;

&lt;h3 id=&quot;32-implementation-challenges-in-nextjs&quot;&gt;3.2 Implementation Challenges in Next.js&lt;/h3&gt;

&lt;p&gt;While conceptually simple, implementing this in a Next.js/Drizzle application is fraught with architectural difficulties, primarily due to the &lt;strong&gt;Connection Context Problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In a typical Next.js app, the database client is a global singleton imported by Server Actions:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// src/lib/db.ts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;drizzle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postgres&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DATABASE_URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When a test runs, it typically initiates a transaction on its own connection instance. However, when the test calls a Server Action, that action imports the global &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db&lt;/code&gt; instance, which usually maintains its own connection pool. Consequently, the Server Action executes its queries outside the test’s transaction. The changes are committed to the database, polluting the state for subsequent tests and rendering the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ROLLBACK&lt;/code&gt; ineffective.&lt;/p&gt;

&lt;h3 id=&quot;33-the-proxy--asynclocalstorage-solution&quot;&gt;3.3 The Proxy &amp;amp; AsyncLocalStorage Solution&lt;/h3&gt;

&lt;p&gt;To make Transactional Rollbacks work in Node.js/Next.js, one must force the application code to use the test’s transaction connection instead of the global pool. This requires a form of “Context Propagation.”&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AsyncLocalStorage&lt;/code&gt; (ALS) from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node:async_hooks&lt;/code&gt; module provides a mechanism to store data that is unique to the current asynchronous execution context (similar to Thread-Local Storage in multi-threaded languages).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation Architecture:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Wrapper&lt;/strong&gt;: Create a database accessor that checks the ALS store.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AsyncLocalStorage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;node:async_hooks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;txStorage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AsyncLocalStorage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getDb&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;txStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tx&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tx&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;globalDb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The Test Hook&lt;/strong&gt;: In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beforeEach&lt;/code&gt;, start a transaction and enter the ALS context.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;example test&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;globalDb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;transaction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;txStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// Inside this block, getDb() returns &apos;tx&apos;&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;myServerAction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; 
      &lt;span class=&quot;nx&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;rollback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Force rollback at end&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Refactoring&lt;/strong&gt;: The application must be refactored to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getDb()&lt;/code&gt; instead of importing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db&lt;/code&gt; directly, or the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db&lt;/code&gt; export must be a Proxy object that handles this logic internally.&lt;/p&gt;

&lt;h3 id=&quot;34-pros-and-cons&quot;&gt;3.4 Pros and Cons&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;Analysis&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Speed&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High. Rollbacks are nearly instantaneous. No file I/O or schema rebuilding is required between tests.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Parallelism&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Low/Complex. Since multiple tests effectively share the same underlying database instance (even if wrapped in transactions), running them in parallel requires careful management of connection limits and locking. It essentially forces serial execution within a single database.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Complexity&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High. Requires invasive changes to the application architecture (using ALS or Proxies) or strict Dependency Injection. Testing “transactional” logic within the app becomes confusing (nesting transactions inside test transactions).&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Fidelity&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Medium. Logic that relies on “after commit” hooks or side effects visible to other connections cannot be tested easily because the transaction is never actually committed.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: While powerful, the Transactional Rollback strategy fights against the grain of the Next.js App Router’s module system. It is best suited for applications that already use heavy Dependency Injection patterns (like NestJS) rather than standard Next.js patterns.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;4-strategy-ii-database-truncation-the-clean-slate-approach&quot;&gt;4. Strategy II: Database Truncation (The “Clean Slate” Approach)&lt;/h2&gt;

&lt;p&gt;The Truncation strategy is the brute-force alternative to rollbacks. Instead of preventing writes from persisting, we allow them to persist and then aggressively clean the database after every test.&lt;/p&gt;

&lt;h3 id=&quot;41-mechanism-of-action&quot;&gt;4.1 Mechanism of Action&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Global Setup&lt;/strong&gt;: A real database is spun up (usually via Docker) and the schema is migrated once at the start of the test suite run.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Test Execution&lt;/strong&gt;: The test runs against this real database. Data is committed.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Teardown (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;afterEach&lt;/code&gt;)&lt;/strong&gt;: A utility function queries the database for all table names and executes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUNCATE&lt;/code&gt; commands to wipe all rows.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;42-handling-foreign-keys-and-sequences&quot;&gt;4.2 Handling Foreign Keys and Sequences&lt;/h3&gt;

&lt;p&gt;A naive &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE FROM table&lt;/code&gt; is insufficient and slow. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUNCATE&lt;/code&gt; is faster as it deallocates data pages. However, relational integrity constraints (Foreign Keys) pose a challenge. You cannot truncate the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;users&lt;/code&gt; table if the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;posts&lt;/code&gt; table references it.&lt;/p&gt;

&lt;p&gt;To solve this, PostgreSQL provides the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CASCADE&lt;/code&gt; option:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;TRUNCATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;posts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;comments&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;CASCADE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Additionally, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUNCATE&lt;/code&gt; does not reset auto-incrementing primary keys (IDENTITY columns) by default. If Test A creates User ID 1, Test B will create User ID 2. If Test B asserts “User ID should be 1”, it will fail. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RESTART IDENTITY&lt;/code&gt; clause is required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimised Drizzle Implementation:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;drizzle-orm&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;resetDatabase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Disable triggers if necessary or use CASCADE&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sql&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`
    SELECT table_name
    FROM information_schema.tables
    WHERE table_schema = &apos;public&apos; AND table_type = &apos;BASE TABLE&apos;
  `&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tables&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Construct a single query to truncate all tables at once&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tableNames&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;table_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;, &lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  
  &lt;span class=&quot;c1&quot;&gt;// RESTART IDENTITY resets sequences&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// CASCADE handles foreign keys&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;raw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`TRUNCATE TABLE &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tableNames&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; RESTART IDENTITY CASCADE`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This script is robust because it dynamically discovers tables, ensuring that newly added tables are automatically cleaned up without updating the test helper.&lt;/p&gt;

&lt;h3 id=&quot;43-the-concurrency-bottleneck&quot;&gt;4.3 The Concurrency Bottleneck&lt;/h3&gt;

&lt;p&gt;The fatal flaw of the Truncation strategy is its incompatibility with parallel testing. Since there is only one shared database instance, you cannot run Test A and Test B simultaneously. If Test A truncates the database while Test B is in the middle of an operation, Test B will fail catastrophically.&lt;/p&gt;

&lt;p&gt;This forces the test runner (e.g., Vitest or Jest) to run in Serial Mode (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--runInBand&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxWorkers=1&lt;/code&gt;). As the application grows to hundreds of integration tests, the feedback loop extends from seconds to minutes, severely hampering developer productivity.&lt;/p&gt;

&lt;h3 id=&quot;44-pros-and-cons&quot;&gt;4.4 Pros and Cons&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;Analysis&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Speed (Per Test)&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Medium. Truncate is faster than DROP/CREATE but slower than ROLLBACK. The database must physically clean pages on disk.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Speed (Total)&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Low. Forces serial execution. Scalability is linear with the number of tests.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Simplicity&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High. Very easy to understand and implement. No “magic” proxies or complex context switching. The database behaves exactly as production.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Reliability&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High. Cleans everything. Less prone to subtle state leaks than complex transaction nesting.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Database Truncation is a viable strategy for smaller projects or CI pipelines where parallelism is not a priority. It is robust and simple but hits a hard ceiling on scalability.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;5-strategy-iii-containerised-isolation-testcontainers&quot;&gt;5. Strategy III: Containerised Isolation (Testcontainers)&lt;/h2&gt;

&lt;p&gt;To solve the concurrency problem of the Truncation strategy without sacrificing the fidelity of a real database, many teams turn to Testcontainers. This library allows the test runner to programmatically spin up disposable Docker containers for dependencies.&lt;/p&gt;

&lt;h3 id=&quot;51-the-architecture-of-ephemeral-infrastructure&quot;&gt;5.1 The Architecture of Ephemeral Infrastructure&lt;/h3&gt;

&lt;p&gt;Testcontainers for Node.js wraps the Docker API. It allows a test to:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Request a PostgreSQL image (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres:16-alpine&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;Start the container on a random, available port.&lt;/li&gt;
  &lt;li&gt;Wait for the database to be ready (using log strategies or health checks).&lt;/li&gt;
  &lt;li&gt;Return the connection string to the application.&lt;/li&gt;
  &lt;li&gt;Destroy the container automatically when the test finishes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The “Ryuk” sidecar container is a special component of Testcontainers that ensures cleanup. It monitors the connection to the test runner; if the runner process dies (even unexpectedly), Ryuk kills the spawned containers, preventing “zombie” processes from consuming system resources.&lt;/p&gt;

&lt;h3 id=&quot;52-implementation-modes&quot;&gt;5.2 Implementation Modes&lt;/h3&gt;

&lt;p&gt;There are two ways to deploy this for Next.js testing:&lt;/p&gt;

&lt;h4 id=&quot;521-singleton-container-with-logical-isolation&quot;&gt;5.2.1 Singleton Container with Logical Isolation&lt;/h4&gt;

&lt;p&gt;Spinning up a Docker container takes time (500ms - 2s). Doing this for every test file is often too slow. A common pattern is to start one container for the entire test suite (global setup). Then, for each test worker, create a unique logical database inside that container.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Setup&lt;/strong&gt;: Container starts at port 5432.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Worker 1&lt;/strong&gt;: Connects, runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CREATE DATABASE test_db_1&lt;/code&gt;, migrates, runs tests.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Worker 2&lt;/strong&gt;: Connects, runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CREATE DATABASE test_db_2&lt;/code&gt;, migrates, runs tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows parallelism. However, it requires complex orchestration code to manage unique database names and ensure migrations are run for every new logical database created.&lt;/p&gt;

&lt;h4 id=&quot;522-container-per-test-file&quot;&gt;5.2.2 Container Per Test File&lt;/h4&gt;

&lt;p&gt;The most isolated approach is to give every test file its own container.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Perfect isolation. No shared state.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cons&lt;/strong&gt;: Heavy Resource Usage. Running 10 parallel test files means running 10 PostgreSQL containers simultaneously. This requires significant RAM and CPU. On a typical CI agent (with 2 vCPUs and 4GB RAM), this will likely cause crashes or extreme slowness due to context switching.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;53-drizzle-integration&quot;&gt;5.3 Drizzle Integration&lt;/h3&gt;

&lt;p&gt;Connecting Drizzle to a Testcontainer is straightforward since Drizzle accepts any standard connection string.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PostgreSqlContainer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@testcontainers/postgresql&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;drizzle&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;drizzle-orm/node-postgres&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// In global setup&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;container&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PostgreSqlContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;postgres:16&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;connectionString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getConnectionUri&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;drizzle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;54-pros-and-cons&quot;&gt;5.4 Pros and Cons&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;Analysis&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Fidelity&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Maximum. You are testing against the exact binary version used in production. Extensions (PostGIS, pgvector) work perfectly.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Isolation&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High. Containers provide process-level isolation.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Low to Medium. Docker startup overhead is the main bottleneck. High resource consumption limits the degree of parallelism possible on standard hardware.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Complex. Requires a Docker daemon availability. This can be challenging in certain CI environments (e.g., Docker-in-Docker setups) or restricted corporate laptops.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Testcontainers is the industry standard for End-to-End (E2E) tests where accuracy is more important than speed. For integration tests that run frequently (e.g., on every file save), it is often too heavy.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;6-strategy-iv-in-memory-webassembly-databases-pglite&quot;&gt;6. Strategy IV: In-Memory WebAssembly Databases (PGlite)&lt;/h2&gt;

&lt;p&gt;The most recent and transformative development in this space is the emergence of PGlite. This technology fundamentally shifts the “Latency vs. Throughput” curve by offering the isolation of containers with the speed of in-memory execution.&lt;/p&gt;

&lt;h3 id=&quot;61-what-is-pglite&quot;&gt;6.1 What is PGlite?&lt;/h3&gt;

&lt;p&gt;PGlite is a build of PostgreSQL compiled to WebAssembly (WASM). Unlike mocks (which fake the database) or SQLite (which is a different database entirely), PGlite runs the actual Postgres query engine code. It executes within the Node.js process memory. It does not require a daemon, a network port, or Docker.&lt;/p&gt;

&lt;p&gt;Because it is just a JavaScript object consuming memory, starting a PGlite instance takes milliseconds (~10-50ms), compared to seconds for a Docker container.&lt;/p&gt;

&lt;h3 id=&quot;62-architecture-for-massive-parallelism&quot;&gt;6.2 Architecture for Massive Parallelism&lt;/h3&gt;

&lt;p&gt;PGlite enables a “Database per Test File” architecture without the resource penalty of Docker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration with Vitest:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vitest is a modern test runner that supports multi-threading. By combining Vitest’s parallelism with PGlite, we can achieve perfect isolation.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Vitest Config&lt;/strong&gt;: Configure the test pool to use threads or forks. This ensures each test file runs in its own worker.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Mocking the Database&lt;/strong&gt;: We use Vitest’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vi.mock&lt;/code&gt; to intercept imports to the application’s database module.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Setup File&lt;/strong&gt;: In the setup for each test file, we instantiate a new PGlite database.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// vitest.setup.ts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;vi&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;vitest&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PGlite&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@electric-sql/pglite&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;drizzle&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;drizzle-orm/pglite&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;migrate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;drizzle-orm/pglite/migrator&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Mock the module that exports the singleton &apos;db&apos;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;vi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@/lib/db&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// 1. Create a fresh in-memory Postgres instance&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PGlite&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; 
  
  &lt;span class=&quot;c1&quot;&gt;// 2. Connect Drizzle&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;drizzle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// 3. Apply Schema (using Drizzle&apos;s push or migrate)&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Note: PGlite is fast enough to run migrations per test file&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;migrate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;migrationsFolder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./drizzle&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;63-the-memory-filesystem-mechanism&quot;&gt;6.3 The “Memory Filesystem” Mechanism&lt;/h3&gt;

&lt;p&gt;PGlite can persist data to a virtual filesystem in memory (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memory://&lt;/code&gt;). This means that when the test file finishes and the worker process terminates, the memory is reclaimed, and the database effectively vanishes. There is no cleanup required—no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DROP DATABASE&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUNCATE&lt;/code&gt; calls are needed. This “fire and forget” model drastically simplifies the test lifecycle.&lt;/p&gt;

&lt;h3 id=&quot;64-handling-server-actions-with-pglite&quot;&gt;6.4 Handling Server Actions with PGlite&lt;/h3&gt;

&lt;p&gt;Since Next.js Server Actions are just functions imported into the test, they will transparently use the mocked &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db&lt;/code&gt; module. When &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;myServerAction()&lt;/code&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db.insert(...)&lt;/code&gt;, it is inserting into the local PGlite instance running in the test thread. Because each test thread has its own mocked instance, they can run fully in parallel without any risk of data collision.&lt;/p&gt;

&lt;h3 id=&quot;65-performance-benchmarks&quot;&gt;6.5 Performance Benchmarks&lt;/h3&gt;

&lt;p&gt;Comparative benchmarks reveal the efficiency of this approach:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Startup Time&lt;/strong&gt;: PGlite starts in ~20ms vs Testcontainers ~1500ms.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Throughput&lt;/strong&gt;: In a suite of 100 test files, PGlite allows running as many workers as CPU cores (e.g., 8 or 16). Testcontainers might be limited to 2-3 concurrent containers before saturating RAM.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Execution&lt;/strong&gt;: CRUD operations in PGlite are slightly slower than native Postgres due to WASM overhead, but the elimination of network latency (localhost TCP loopback) often compensates for this in integration test scenarios.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;66-pros-and-cons&quot;&gt;6.6 Pros and Cons&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;Analysis&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Speed&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Very High. Instant startup enables a “Database per Test File” strategy.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Isolation&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Perfect. Every test file gets a completely independent, shared-nothing database instance.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Parallelism&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Excellent. Scaling is limited only by CPU cores, not RAM or Docker limits.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Compatibility&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High. Supports most Postgres features (JSONB, Triggers). However, as a WASM build, it is single-process (no multi-connection testing) and may lack support for specific native extensions like PostGIS unless custom-built.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Dev Experience&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Superior. No need to install Docker. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; is all that is needed to run the test suite.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: For 95% of Next.js applications using Drizzle, PGlite is the optimal strategy. It provides the best balance of speed, isolation, and developer experience.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;7-data-management-seeding-and-determinism&quot;&gt;7. Data Management: Seeding and Determinism&lt;/h2&gt;

&lt;p&gt;Regardless of the isolation strategy chosen, integration tests require data. “Seeding” is the process of populating the database with the necessary state (users, organisations, items) before a test runs.&lt;/p&gt;

&lt;h3 id=&quot;71-the-drizzle-seed-library&quot;&gt;7.1 The Drizzle Seed Library&lt;/h3&gt;

&lt;p&gt;The Drizzle team recently introduced &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;drizzle-seed&lt;/code&gt;, a library designed to solve the problem of generating realistic, relational data. Unlike traditional faker libraries which are purely random, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;drizzle-seed&lt;/code&gt; uses a seedable pseudo-random number generator (pRNG).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Determinism Matters:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In testing, you want randomness to cover edge cases, but you also want reproducibility. If a test fails because a random name contained an emoji that broke your validation, you need to be able to reproduce that exact failure. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;drizzle-seed&lt;/code&gt; allows you to set a specific seed (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;seed: 12345&lt;/code&gt;). Every time the test runs with that seed, it generates the exact same set of “random” data.&lt;/p&gt;

&lt;h3 id=&quot;72-relational-integrity&quot;&gt;7.2 Relational Integrity&lt;/h3&gt;

&lt;p&gt;Generating data for relational databases is hard because of foreign keys. You can’t create a Post without a User. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;drizzle-seed&lt;/code&gt; introspects your Drizzle schema relationships.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;seed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;refine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;funcs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;columns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;funcs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// deterministic emails&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;posts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Distributed among the 5 users&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This automatically handles the foreign key dependency, ensuring that every post created is linked to a valid user ID from the users generation step.&lt;/p&gt;

&lt;h3 id=&quot;73-factories-vs-global-seeds&quot;&gt;7.3 Factories vs. Global Seeds&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Global Seeding&lt;/strong&gt;: Populating the DB with a massive set of “standard” data at the start of the suite. This is good for read-heavy tests but bad for isolation (tests share the seed state).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Factories (Recommended)&lt;/strong&gt;: Using helper functions within each test to create only the data needed for that specific test.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// test/factories.ts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createUser&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;overrides&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;defaultData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;overrides&lt;/span&gt; 
  &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;returning&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Combined with PGlite, this is extremely fast. Since the DB is empty at the start of the test file, the test creates 1 user, asserts against it, and finishes.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;8-comparative-analysis--decision-matrix&quot;&gt;8. Comparative Analysis &amp;amp; Decision Matrix&lt;/h2&gt;

&lt;p&gt;To guide the selection of the appropriate strategy, we synthesise the findings into a decision matrix.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Criterion&lt;/th&gt;
      &lt;th&gt;Transactional Rollback&lt;/th&gt;
      &lt;th&gt;Database Truncation&lt;/th&gt;
      &lt;th&gt;Testcontainers&lt;/th&gt;
      &lt;th&gt;PGlite (WASM)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Isolation Quality&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Medium (Connection sharing risks)&lt;/td&gt;
      &lt;td&gt;High (If serial) / Low (If parallel)&lt;/td&gt;
      &lt;td&gt;High (Process Isolation)&lt;/td&gt;
      &lt;td&gt;High (Memory Isolation)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Parallelism&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Low (Difficult to implement)&lt;/td&gt;
      &lt;td&gt;None (Must be Serial)&lt;/td&gt;
      &lt;td&gt;Medium (Resource constraints)&lt;/td&gt;
      &lt;td&gt;High (CPU bound)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Startup Overhead&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Fast&lt;/td&gt;
      &lt;td&gt;Fast&lt;/td&gt;
      &lt;td&gt;Slow (Docker spin-up)&lt;/td&gt;
      &lt;td&gt;Instant&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;CI Infrastructure&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Minimal&lt;/td&gt;
      &lt;td&gt;Minimal&lt;/td&gt;
      &lt;td&gt;Heavy (Needs Docker)&lt;/td&gt;
      &lt;td&gt;Minimal (Node only)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Production Parity&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High&lt;/td&gt;
      &lt;td&gt;High&lt;/td&gt;
      &lt;td&gt;Exact&lt;/td&gt;
      &lt;td&gt;High (High fidelity simulation)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Implementation&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Complex (Proxies/ALS)&lt;/td&gt;
      &lt;td&gt;Simple&lt;/td&gt;
      &lt;td&gt;Moderate&lt;/td&gt;
      &lt;td&gt;Moderate (Mocking)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;81-recommendations&quot;&gt;8.1 Recommendations&lt;/h3&gt;

&lt;h4 id=&quot;for-new-greenfield-nextjs-applications&quot;&gt;For New (Greenfield) Next.js Applications&lt;/h4&gt;

&lt;p&gt;Adopt the &lt;strong&gt;PGlite Strategy&lt;/strong&gt;. The benefits of parallel execution and instant startup significantly outweigh the minor complexity of setting up the mock. It allows your test suite to grow to thousands of tests without slowing down your CI pipeline. It aligns perfectly with the isolated module structure of Next.js Server Actions.&lt;/p&gt;

&lt;h4 id=&quot;for-applications-with-specific-postgres-extensions-eg-postgis&quot;&gt;For Applications with Specific Postgres Extensions (e.g., PostGIS)&lt;/h4&gt;

&lt;p&gt;Use &lt;strong&gt;Testcontainers&lt;/strong&gt;. PGlite currently has limited support for complex C-based extensions. If your app relies heavily on geospatial queries or vector similarity search (pgvector), the fidelity of a real Dockerised Postgres instance is non-negotiable. To mitigate the slowness, consider sharding your tests in CI (running different test files on different machines).&lt;/p&gt;

&lt;h4 id=&quot;for-legacy-applications-or-small-suites&quot;&gt;For Legacy Applications or Small Suites&lt;/h4&gt;

&lt;p&gt;Use &lt;strong&gt;Database Truncation&lt;/strong&gt;. If you already have a working Postgres setup and your test suite takes less than 2 minutes to run serially, the effort to migrate to PGlite may not yield immediate ROI. Truncation is simple, reliable, and “good enough” for small scales.&lt;/p&gt;

&lt;h4 id=&quot;avoid-transactional-rollbacks-in-nextjs&quot;&gt;Avoid Transactional Rollbacks in Next.js&lt;/h4&gt;

&lt;p&gt;The impedance mismatch between the “Stateless Request” model of Next.js Server Actions and the “Stateful Connection” model of SQL transactions makes this strategy brittle. It typically leads to “magic” code (Proxies, ALS) that is hard to debug and hard to onboard new developers to.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;9-conclusion&quot;&gt;9. Conclusion&lt;/h2&gt;

&lt;p&gt;The landscape of integration testing in the Next.js and Drizzle ecosystem has shifted from managing shared resources to provisioning ephemeral ones. The “old way” of managing a single database and carefully cleaning it up is being superseded by the “new way” of creating disposables.&lt;/p&gt;

&lt;p&gt;Technological advancements—specifically containerisation and WebAssembly—have given us new tools. Testcontainers allows us to treat a database server as an ephemeral object, while PGlite allows us to treat the entire database concept as a disposable JavaScript variable.&lt;/p&gt;

&lt;p&gt;For the modern Next.js developer, the PGlite approach represents a sweet spot. It respects the boundaries of the framework, enables the blazing speed of Drizzle’s query generation, and provides the isolation necessary to banish flaky tests forever. By combining Vitest’s threading, PGlite’s in-memory speed, and Drizzle’s schema management, teams can build test suites that are not just safety nets, but accelerators of development velocity.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Next.js and Drizzle Integration&lt;/li&gt;
  &lt;li&gt;PGlite Architecture and Benchmarks&lt;/li&gt;
  &lt;li&gt;Testcontainers Usage and Comparison&lt;/li&gt;
  &lt;li&gt;Truncation and Seeding Logic&lt;/li&gt;
  &lt;li&gt;Transaction Management in Drizzle&lt;/li&gt;
  &lt;li&gt;Server-Only Constraints in Next.js&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 25 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://nihilok.github.io//test-isolation-for-a-nextjs-app-with-drizzle-orm</link>
        <link href="https://nihilok.github.io/test-isolation-for-a-nextjs-app-with-drizzle-orm"/>
        <guid isPermaLink="true">https://nihilok.github.io/test-isolation-for-a-nextjs-app-with-drizzle-orm</guid>
      </item>
    
      <item>
        <title>Why I Built Another Task Runner</title>
        <description>&lt;p&gt;Yes, I know. Another task runner. In 2026. Let me explain why.&lt;/p&gt;

&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;/h2&gt;

&lt;p&gt;I’m not a Make expert. But somehow I became the person people ask when they need to add a task to our Makefile.&lt;/p&gt;

&lt;p&gt;Last week it was “How do I pass an environment variable to this target?” The week before: “Why does this only work if I add a tab character?” And the week before that it was me forgetting to add a task to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.PHONY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every time, I’m Googling the same things they could be Googling. Our build process has things like this:&lt;/p&gt;

&lt;div class=&quot;language-makefile highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nl&quot;&gt;deploy-%&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;guard-% check-git-clean&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt; ENV :&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;word&lt;/span&gt; 2,&lt;span class=&quot;nf&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;subst&lt;/span&gt; -, ,&lt;span class=&quot;nv&quot;&gt;$@&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;)))&lt;/span&gt;
	./scripts/deploy.sh &lt;span class=&quot;nv&quot;&gt;$(ENV)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you understand what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$(word 2,$(subst -, ,$@))&lt;/code&gt; does without looking it up, congratulations — you’re in the 1% of developers who’ve memorised Make’s arcane variable substitution syntax.&lt;/p&gt;

&lt;h2 id=&quot;the-alternatives-werent-much-better&quot;&gt;The Alternatives Weren’t Much Better&lt;/h2&gt;

&lt;p&gt;Other repos in the company use npm scripts. Some have custom Python CLI packages run with uv. Every project has its own approach.&lt;/p&gt;

&lt;p&gt;The npm scripts repos hit the same cross-platform issues. Shell commands that work fine on Mac and Linux fail on Windows. You end up with:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;clean&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;rm -rf dist || rmdir /s /q dist&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;NODE_ENV=production webpack || set NODE_ENV=production &amp;amp;&amp;amp; webpack&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Ugly. Fragile. And it still broke in random edge cases.&lt;/p&gt;

&lt;p&gt;So I looked at the established alternatives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just&lt;/strong&gt; (~22k stars) is the closest thing to what I wanted: a clean &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;justfile&lt;/code&gt; with Make-inspired syntax, recipe parameters, shell completions. But parameters have no type annotations — they’re positional strings and that’s it. Handy for humans, but AI agents can’t reliably introspect them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task (go-task)&lt;/strong&gt; (~15k stars) impressed me with its built-in POSIX shell interpreter, which gives genuinely consistent cross-platform behaviour without relying on the system shell. But it’s shell-only — no inline Python or Node.js. And MCP support is still just a GitHub issue with no implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mise&lt;/strong&gt; (~25k stars) is arguably the most feature-rich tool in this space, combining dev tool version management (replacing asdf/nvm/pyenv), environment management, and task running in a single binary. It does have an experimental &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mise mcp&lt;/code&gt; server, which is genuinely interesting — though it exposes the broader dev environment surface rather than being focused specifically on task execution. For teams already using mise for version management, that breadth is a strength.&lt;/p&gt;

&lt;p&gt;The Markdown-based runners — &lt;strong&gt;Mask&lt;/strong&gt;, &lt;strong&gt;xc&lt;/strong&gt;, &lt;strong&gt;Maid&lt;/strong&gt; — are a compelling idea (documentation and automation in one file), but none of them have any MCP story, and Maid hasn’t been meaningfully maintained in years.&lt;/p&gt;

&lt;h2 id=&quot;what-i-actually-wanted&quot;&gt;What I Actually Wanted&lt;/h2&gt;

&lt;p&gt;I wanted to write tasks the way I think about them:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Just deploy the thing&lt;/span&gt;
deploy&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;environment&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    ./scripts/deploy.sh &lt;span class=&quot;nv&quot;&gt;$environment&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But I also wanted:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Python when I needed real logic (not bash’s string manipulation nightmare)&lt;/li&gt;
  &lt;li&gt;Node when working with JSON or async operations&lt;/li&gt;
  &lt;li&gt;Cross-platform support without conditional hell&lt;/li&gt;
  &lt;li&gt;Something AI agents could actually use — not as an afterthought, but as a first-class feature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point turned out to be the gap nobody had filled properly.&lt;/p&gt;

&lt;h2 id=&quot;so-i-built-run&quot;&gt;So I Built &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;So I built &lt;a href=&quot;https://runtool.dev&quot;&gt;run&lt;/a&gt;. A Runfile looks like this:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Shell for simple stuff&lt;/span&gt;
build&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; cargo build &lt;span class=&quot;nt&quot;&gt;--release&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Python when you need it&lt;/span&gt;
analyze&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;#!/usr/bin/env python&lt;/span&gt;
    import json
    with open&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; as f:
        data &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; json.load&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;f&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        print&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;f&lt;span class=&quot;s2&quot;&gt;&quot;Processed {len(data)} records&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Node works too&lt;/span&gt;
process&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;#!/usr/bin/env node&lt;/span&gt;
    const fs &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; require&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;fs&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    console.log&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Processing...&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Platform-specific versions&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# @os windows&lt;/span&gt;
deploy&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;environment&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    .&lt;span class=&quot;se&quot;&gt;\s&lt;/span&gt;cripts&lt;span class=&quot;se&quot;&gt;\d&lt;/span&gt;eploy.ps1 &lt;span class=&quot;nv&quot;&gt;$environment&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# @os linux darwin&lt;/span&gt;
deploy&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;environment&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    ./scripts/deploy.sh &lt;span class=&quot;nv&quot;&gt;$environment&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;No YAML. No TOML. No &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$(word 2,$(subst -, ,$@))&lt;/code&gt;. Just functions with named parameters.&lt;/p&gt;

&lt;h3 id=&quot;polyglot-argument-passing&quot;&gt;Polyglot Argument Passing&lt;/h3&gt;

&lt;p&gt;The polyglot model is worth dwelling on — even if you never touch the AI features, this alone is a meaningful improvement over existing tools.&lt;/p&gt;

&lt;p&gt;Notice how the Python function above gets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file&lt;/code&gt; as a native Python variable — no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sys.argv[1]&lt;/code&gt;, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;argparse&lt;/code&gt;, no parsing. And it goes further than just strings. If you add a type annotation:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;resize&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;file, width: int, height: int&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;#!/usr/bin/env python&lt;/span&gt;
    from PIL import Image
    img &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; Image.open&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    img.resize&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;width, height&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;.save&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    print&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;f&lt;span class=&quot;s2&quot;&gt;&quot;Resized to {width}x{height}&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;width&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;height&lt;/code&gt; arrive as actual Python &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int&lt;/code&gt;s. You can multiply them, pass them to APIs that expect numbers, use them in range expressions — no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int(sys.argv[2])&lt;/code&gt; boilerplate. The same applies to Node.js, Ruby, and PowerShell: RunTool generates the appropriate variable declarations for each interpreter, with type-correct values.&lt;/p&gt;

&lt;p&gt;Compare this to the typical polyglot task runner experience: you write a shebang script, and everything arrives as a string. You spend the first few lines of every script parsing and converting arguments. It’s not a lot of code, but it’s friction — and it’s the kind of friction that makes people reach for a full CLI framework instead of a quick task.&lt;/p&gt;

&lt;p&gt;Just supports multiple languages too, via shebangs — but each shebang recipe has to be a separate named recipe. RunTool lets you mix languages within a single file using function-scoped shebangs. And the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@os&lt;/code&gt; attribute lets you define the &lt;em&gt;same function name&lt;/em&gt; with different implementations per platform, which is more ergonomic than Task’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;platforms&lt;/code&gt; filter or mise’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run_windows&lt;/code&gt; property.&lt;/p&gt;

&lt;h2 id=&quot;the-ai-integration&quot;&gt;The AI Integration&lt;/h2&gt;

&lt;p&gt;Here’s where it gets interesting: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt; has a built-in MCP (Model Context Protocol) server.&lt;/p&gt;

&lt;p&gt;I was pairing with Claude Code on some refactoring (&lt;em&gt;it&lt;/em&gt; was driving 🙈) when I realised: it would be helpful if Claude could just run our tests or deployment checks directly. With MCP support, AI agents can discover and execute your project’s tools automatically, and use the exact same tools that you use.&lt;/p&gt;

&lt;p&gt;Add some metadata:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# @desc Deploy to specified environment&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# @arg environment Target environment (staging|prod)&lt;/span&gt;
deploy&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;environment&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    ./scripts/deploy.sh &lt;span class=&quot;nv&quot;&gt;$environment&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The function signature &lt;em&gt;is&lt;/em&gt; the schema. Claude (or any MCP-compatible agent) sees a tool called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy&lt;/code&gt; with a required &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;environment&lt;/code&gt; parameter — no guessing, no multi-step discovery, no verbose Markdown explanations needed. Type annotations and defaults work too:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# @desc Scale a service&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# @arg service The service name&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# @arg replicas Number of instances&lt;/span&gt;
scale&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;service, replicas: int &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 1&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    docker compose scale &lt;span class=&quot;nv&quot;&gt;$service&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$replicas&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@desc&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@arg&lt;/code&gt; annotations serve dual purposes: they generate human-readable help text &lt;em&gt;and&lt;/em&gt; provide structured metadata that MCP clients use for tool discovery and invocation. That’s architecturally different from Just’s approach, where three independent community-built MCP servers have to parse the justfile format externally without any structured argument metadata to work with.&lt;/p&gt;

&lt;p&gt;There’s a useful security property here too: the MCP server only exposes the annotations — the function name, description, and argument signatures. The implementation is not surfaced. An agent knows that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy&lt;/code&gt; takes an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;environment&lt;/code&gt; argument of type string; it doesn’t know (or need to know) that internally it shells out to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./scripts/deploy.sh&lt;/code&gt;, or that there’s a secrets file being sourced, or any other implementation detail. If the agent wants to read the Runfile itself it can, but that’s an explicit read operation — not something the MCP server hands over automatically. You can give agents access to your tooling without giving them a map of your internals.&lt;/p&gt;

&lt;p&gt;As of early 2026, the landscape looks like this:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Tool&lt;/th&gt;
      &lt;th&gt;Polyglot scripting&lt;/th&gt;
      &lt;th&gt;Typed arguments&lt;/th&gt;
      &lt;th&gt;MCP integration&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;RunTool&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;✅ Shell/Python/Node/Ruby/PS&lt;/td&gt;
      &lt;td&gt;✅ Signature types + defaults&lt;/td&gt;
      &lt;td&gt;✅ Built-in&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Just&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;✅ Via shebangs&lt;/td&gt;
      &lt;td&gt;⚠️ Params, no types&lt;/td&gt;
      &lt;td&gt;⚠️ 3 third-party servers&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Task&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;❌ Shell-only&lt;/td&gt;
      &lt;td&gt;⚠️ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requires&lt;/code&gt; + enum&lt;/td&gt;
      &lt;td&gt;❌ Requested only&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Mise&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;✅ Via shebangs&lt;/td&gt;
      &lt;td&gt;✅ usage spec&lt;/td&gt;
      &lt;td&gt;✅ Built-in (experimental)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Mask&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;✅ Code block langs&lt;/td&gt;
      &lt;td&gt;⚠️ Flags with types&lt;/td&gt;
      &lt;td&gt;❌ None&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;xc&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;❌ Shell-only&lt;/td&gt;
      &lt;td&gt;⚠️ Inputs attribute&lt;/td&gt;
      &lt;td&gt;❌ None&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Make&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;❌ Shell (with quirks)&lt;/td&gt;
      &lt;td&gt;❌ None&lt;/td&gt;
      &lt;td&gt;⚠️ Third-party only&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;RunTool and mise are the only tools with built-in MCP servers. The difference is scope: mise’s MCP server exposes its full environment management surface (tools, tasks, versions, env vars), which is powerful if you want an agent that can manage your entire dev environment. RunTool’s is narrower by design — purpose-built around task execution and structured argument metadata — which means less surface area to audit and a simpler mental model for what the agent can do.&lt;/p&gt;

&lt;h2 id=&quot;zero-dependencies-instant-startup&quot;&gt;Zero Dependencies, Instant Startup&lt;/h2&gt;

&lt;p&gt;It’s a single Rust binary. No runtime. No package.json with 500 dependencies. You run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run deploy staging&lt;/code&gt; and it just works.&lt;/p&gt;

&lt;p&gt;And it comes with shell completions out of the box — bash, zsh, fish, and PowerShell. Tab completion for all your tasks, no extra setup required.&lt;/p&gt;

&lt;h2 id=&quot;honestly-the-tradeoffs&quot;&gt;Honestly, the Tradeoffs&lt;/h2&gt;

&lt;p&gt;I’d be doing you a disservice if I didn’t acknowledge what the alternatives have that RunTool doesn’t — yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just&lt;/strong&gt; has years of battle-testing, a backwards-compatibility guarantee (“there will never be a just 2.0”), and extensive editor support across VS Code, JetBrains, Helix, Kakoune, and Zed. Its community has built three independent MCP servers. That ecosystem trust takes years to build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task&lt;/strong&gt; has something technically impressive that’s easy to overlook: its built-in shell interpreter (mvdan/sh) plus built-in Unix utilities means it’s genuinely cross-platform without platform-specific shims. If your team has Windows developers and you’ve suffered through shell compatibility issues, that’s a compelling argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mise&lt;/strong&gt; bundles tool version management, environment management, and task running into a single binary — reducing the number of tools a project depends on. If you’re already using mise for version management, its experimental MCP support and rich task runner make it the most comprehensive single-binary solution. And its broader MCP surface area is genuinely useful if you want agents that can manage your full dev environment, not just run tasks.&lt;/p&gt;

&lt;p&gt;RunTool is a new entrant betting that AI-agent interoperability will become a first-class concern for developer tooling. That bet might be right or wrong. What I can say is: when I’m working alongside Claude Code on a project and it can discover and run the project’s tasks through the same interface I do, that workflow genuinely changes how I work.&lt;/p&gt;

&lt;h2 id=&quot;is-this-useful-to-anyone-else&quot;&gt;Is This Useful to Anyone Else?&lt;/h2&gt;

&lt;p&gt;I don’t know. Maybe you’re happy with Make. Maybe Just works perfectly for you. Maybe mise already does everything you need. Maybe you don’t care whether your AI agent can introspect your task arguments.&lt;/p&gt;

&lt;p&gt;But if you’ve ever thought “there has to be a simpler way to do this” — whether that’s passing typed arguments to a Python task without &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;argparse&lt;/code&gt;, writing cross-platform tasks without conditional hell, or giving your AI agent access to your project’s tools — maybe give it a try:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;nihilok/tap/runtool
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;or&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cargo &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;run
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The code is on &lt;a href=&quot;https://github.com/nihilok/run&quot;&gt;GitHub&lt;/a&gt;. It solves my problems. Maybe it’ll solve yours too.&lt;/p&gt;
</description>
        <pubDate>Sat, 17 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://nihilok.github.io//why-i-built-another-task-runner</link>
        <link href="https://nihilok.github.io/why-i-built-another-task-runner"/>
        <guid isPermaLink="true">https://nihilok.github.io/why-i-built-another-task-runner</guid>
      </item>
    
      <item>
        <title>Offline Encryption Using WebAuthn</title>
        <description>&lt;p&gt;I’ve been diving deep into browser-based cryptography lately, and implementing truly secure offline encryption is frustratingly complex. The core problem? Getting consistent key material across sessions without storing anything sensitive.&lt;/p&gt;

&lt;p&gt;After countless late nights of cursing at my keyboard, I stumbled upon an intriguing solution: using WebAuthn (e.g. those little USB security keys like YubiKey) not just for authentication, but as a source of encryption key material.&lt;/p&gt;

&lt;h2 id=&quot;the-encryption-challenge&quot;&gt;The Encryption Challenge&lt;/h2&gt;

&lt;p&gt;Creating secure encryption that works completely offline in a browser environment is difficult for several reasons:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Your key material needs to be consistent across browser sessions&lt;/li&gt;
  &lt;li&gt;You can’t just derive keys from code (that’s basically security through obscurity)&lt;/li&gt;
  &lt;li&gt;Browser storage isn’t secure enough for sensitive key material&lt;/li&gt;
  &lt;li&gt;And of course, the whole thing needs to work offline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After banging my head against various approaches (including some truly questionable experiments with IndexedDB), I realised WebAuthn might be the perfect solution. Why? Because those little hardware authenticators never expose their private keys — they’re basically tiny HSMs that live in your USB port!&lt;/p&gt;

&lt;h2 id=&quot;webauthn-not-just-for-logins-anymore&quot;&gt;WebAuthn: Not Just for Logins Anymore&lt;/h2&gt;

&lt;p&gt;WebAuthn wasn’t designed to be a “key extractor” - it’s an authentication standard. But with a bit of creative thinking (and some cryptographic sleight of hand), we can use it to generate consistent encryption material.&lt;/p&gt;

&lt;p&gt;Here’s how it works in a nutshell:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Register a credential&lt;/strong&gt; (one-time setup)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Store the credential ID&lt;/strong&gt; (not secret, so browser storage is fine)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Get an assertion&lt;/strong&gt; using a fixed challenge whenever you need key material&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Use the signature&lt;/strong&gt; as input for your encryption key derivation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The real magic here is that for a given credential and challenge, the hardware authenticator will produce a signature that we can use as key material. The private key never leaves the device!&lt;/p&gt;

&lt;h2 id=&quot;implementation&quot;&gt;Implementation&lt;/h2&gt;

&lt;p&gt;Here’s how to implement the registration phase:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// A randomly generated challenge (only used during registration)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;challenge&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Configure your publicKeyCredentialCreationOptions&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;publicKeyOptions&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;challenge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;challenge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;rp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Offline Demo RP&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;unique-user-id&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;charCodeAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;demo-user&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;displayName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Demo User&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;pubKeyCredParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;public-key&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;alg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ECDSA with SHA-256&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;attestation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;credentials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;publicKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;publicKeyOptions&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cred&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Save the credential ID for future assertions&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;credId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cred&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;rawId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Persist credId (e.g., in IndexedDB)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Registration done, credentialId:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bufferToBase64Url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;credId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bufferToBase64Url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;str&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fromCharCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;btoa&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\+&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\/&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/=+$/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And here’s the really interesting part - getting key material later:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Retrieve the previously saved credentialId&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;savedCredId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;cm&quot;&gt;/* retrieve your stored credentialId */&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;credIdBuffer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64UrlToBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;savedCredId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Use a deterministically derived challenge&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fixedChallenge&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt; &lt;span class=&quot;cm&quot;&gt;/* 32 bytes of fixed challenge data */&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;publicKeyRequestOptions&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;challenge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fixedChallenge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;allowCredentials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;credIdBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;public-key&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;userVerification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;discouraged&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60000&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;credentials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;publicKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;publicKeyRequestOptions&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;assertion&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Extract the signature&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signature&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;assertion&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// Combine with additional secret entropy&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// const combinedMaterial = concat(signature, additionalEntropy);&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// const rawKeyMaterial = await deriveRawKeyMaterial(combinedMaterial);&lt;/span&gt;
    
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Obtained signature for key derivation:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Continue with key derivation...&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64UrlToBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;base64UrlString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64UrlString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/-/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/_/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pad&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;padded&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pad&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pad&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;binary&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;atob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;padded&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;binary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;binary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;char&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;char&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;charCodeAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;but-wait-theres-a-catch&quot;&gt;But Wait, There’s a Catch!&lt;/h2&gt;

&lt;p&gt;Of course there is. When is crypto ever straightforward?&lt;/p&gt;

&lt;p&gt;Some authenticators don’t produce deterministic signatures for the same input. They might:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Use non-deterministic signature algorithms (looking at you, ECDSA)&lt;/li&gt;
  &lt;li&gt;Embed signature counters in the output&lt;/li&gt;
  &lt;li&gt;Do other vendor-specific weird stuff&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means that your key material might change slightly between uses. Not ideal for encryption!&lt;/p&gt;

&lt;p&gt;The solution is a &lt;strong&gt;fuzzy extractor&lt;/strong&gt; — a cryptographic primitive that takes “noisy” input and reliably produces consistent output.&lt;/p&gt;

&lt;p&gt;A typical fuzzy extractor has two phases:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Generate&lt;/strong&gt;: Takes initial input, produces a key and a “helper”&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Reproduce&lt;/strong&gt;: Uses the helper to recover the same key from slightly different input&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;security-considerations-the-javascript-memory-problem&quot;&gt;Security Considerations: The JavaScript Memory Problem&lt;/h2&gt;

&lt;p&gt;One important consideration: JavaScript memory.&lt;/p&gt;

&lt;p&gt;Since all your variables in JavaScript are potentially accessible to attackers with debugging capabilities, your signature values could be exposed if someone can run a debugger or inject code through XSS.&lt;/p&gt;

&lt;p&gt;Some mitigation strategies:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Keep sensitive variables around for as short a time as possible&lt;/li&gt;
  &lt;li&gt;Use Web Crypto APIs which operate outside typical JavaScript memory&lt;/li&gt;
  &lt;li&gt;Implement strict Content Security Policies&lt;/li&gt;
  &lt;li&gt;Clear sensitive data ASAP&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And for extra security, consider:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Adding user-provided entropy (like a password) to your key derivation&lt;/li&gt;
  &lt;li&gt;Never, ever, ever storing sensitive key material in browser storage&lt;/li&gt;
  &lt;li&gt;Considering the security of the device itself in your threat model&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h2&gt;

&lt;p&gt;So there you have it - WebAuthn hardware authenticators can be cleverly repurposed as key material providers for offline encryption. The private key stays safely locked away in hardware, your encryption keys become consistently derivable without network connectivity, and you get to feel like a crypto wizard in the process.&lt;/p&gt;

&lt;p&gt;I’ve been using this technique successfully in a few personal projects, and while it’s definitely not the most conventional approach, it’s been surprisingly robust in practice. The combination of hardware security and fully offline operation makes it a winner for certain types of applications.&lt;/p&gt;

&lt;p&gt;Give it a try in your next offline encryption project, and let me know if you come up with any clever improvements to the technique! Just remember - no crypto system is perfect, so always consider your specific threat model and use case.&lt;/p&gt;

&lt;p&gt;P.S. Huge thanks to the WebAuthn standard creators - I’m pretty sure this wasn’t what they had in mind, but that’s the beauty of well-designed crypto primitives: they enable use cases the designers never even imagined!&lt;/p&gt;
</description>
        <pubDate>Sat, 08 Mar 2025 00:00:00 +0000</pubDate>
        <link>https://nihilok.github.io//offline-only-encryption</link>
        <link href="https://nihilok.github.io/offline-only-encryption"/>
        <guid isPermaLink="true">https://nihilok.github.io/offline-only-encryption</guid>
      </item>
    
      <item>
        <title>Loading tmux when loading shell</title>
        <description>&lt;p&gt;This is something I’ve had mixed success with in the past. I’ve had things that worked but not quite in the way I wanted them to, and other things that just didn’t work! (Try sticking an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exec&lt;/code&gt; before an error in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.zshrc&lt;/code&gt;, and you’ve just nerfed your shell startup!)&lt;/p&gt;

&lt;p&gt;There are a few things we need to consider, e.g. Do we want the same session as last time? What if there is no session? Can we create a new session but attach to an existing one if one by the same name exists? Do we want the same session when on SSH? Or when running a terminal emulator inside an IDE for example?&lt;/p&gt;

&lt;p&gt;I’ve finally nailed it down to the following:&lt;/p&gt;

&lt;div class=&quot;language-zsh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/usr/bin/env zsh&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$TERMINAL_EMULATOR&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;JetBrains-JediTerm&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$TMUX&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
        if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$SSH_CONNECTION&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
            &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;TMUX_SESSION_NAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;tmux_&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SSH_CONNECTION&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;md5sum&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos; &apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; 1 | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; 1-6&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else
            &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;TMUX_SESSION_NAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;tmux&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;fi
        if &lt;/span&gt;tmux has-session &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TMUX_SESSION_NAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
            &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;tmux attach &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TMUX_SESSION_NAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else
            &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;tmux new-session &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TMUX_SESSION_NAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;fi
    fi
fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This script will start a new tmux session if one doesn’t exist, or attach to an existing one if it does. It will also create a new session if you’re on SSH, and it will not run if you’re using the JetBrains terminal emulator (as it doesn’t play nicely with tmux).&lt;/p&gt;

&lt;p&gt;I have this in a separate script, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.load_tmux&lt;/code&gt; which I source from my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.zshrc&lt;/code&gt;, right at the top of the file (so that it is the first thing that is executed):&lt;/p&gt;

&lt;div class=&quot;language-zsh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt; ~/.load_tmux
&lt;span class=&quot;c&quot;&gt;# rest of .zshrc...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Wed, 22 Jan 2025 00:00:00 +0000</pubDate>
        <link>https://nihilok.github.io//loading-tmux-when-loading-shell</link>
        <link href="https://nihilok.github.io/loading-tmux-when-loading-shell"/>
        <guid isPermaLink="true">https://nihilok.github.io/loading-tmux-when-loading-shell</guid>
      </item>
    
      <item>
        <title>Server status monitor bash script</title>
        <description>&lt;p&gt;When managing multiple servers, keeping track of their availability is crucial. Building on an example from “Cybersecurity Ops with bash” by Paul Troncone, I’ve developed a bash script that provides a simple yet effective way to monitor server status across multiple hosts. Let’s break down how this script works and why it can be a valuable tool for system administrators.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# For a given list of servers, this script will check the status of the server and notify the user if the server is down.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Usage: ./server_status.sh [-i &amp;lt;interval&amp;gt;] &amp;lt; server_list.txt&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;declare&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;INTERVAL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;30 &lt;span class=&quot;c&quot;&gt;# Default interval is 30 seconds&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /var/log/server_status.log &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt;
        &lt;span class=&quot;c&quot;&gt;# Check if user has permission to write to /var/log&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; /var/log &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
                &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Cannot write to /var/log/server_status.log; hint: use sudo when running this script for the first time&quot;&lt;/span&gt; 1&amp;gt;&amp;amp;2
                &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
        &lt;span class=&quot;k&quot;&gt;fi
        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch&lt;/span&gt; /var/log/server_status.log
        &lt;span class=&quot;nb&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$USER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; /var/log/server_status.log
        &lt;span class=&quot;nb&quot;&gt;chmod &lt;/span&gt;644 /var/log/server_status.log
&lt;span class=&quot;k&quot;&gt;fi

while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;getopts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;:i:&quot;&lt;/span&gt; opt&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
	case&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;opt&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in
	&lt;/span&gt;i&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;nv&quot;&gt;INTERVAL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$OPTARG&lt;/span&gt;
		&lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
	&lt;span class=&quot;se&quot;&gt;\?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid option: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$OPTARG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; 1&amp;gt;&amp;amp;2
		&lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
	:&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid option: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$OPTARG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; requires an argument&quot;&lt;/span&gt; 1&amp;gt;&amp;amp;2
		&lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;esac&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;shift&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;OPTIND &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done

while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
	&lt;/span&gt;clear
	&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Server Status Monitor&apos;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Status: Scanning ...&apos;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;---------------------------------&apos;&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; server_hostname&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
		&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;server_hostname&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tr&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;\r&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; http&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
            &lt;/span&gt;curl &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; /dev/null &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;%{http_code}&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;(000|404)&apos;&lt;/span&gt; &amp;amp;&amp;gt;/dev/null
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$?&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-eq&lt;/span&gt; 0 &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
                &lt;/span&gt;tput setaf 1
                &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Server: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; is down - &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; /var/log/server_status.log
                tput setaf 7
            &lt;span class=&quot;k&quot;&gt;fi
            continue
        fi

		&lt;/span&gt;ping &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; 1 &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;(Destination Host Unreachable|100% packet loss)&apos;&lt;/span&gt; &amp;amp;&amp;gt;/dev/null
		&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$?&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-eq&lt;/span&gt; 0 &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
			&lt;/span&gt;tput setaf 1
			&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Server: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; is down - &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; /var/log/server_status.log
			tput setaf 7
		&lt;span class=&quot;k&quot;&gt;fi
	done&lt;/span&gt; &amp;lt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;:-&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;/dev/stdin&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Press [CTRL+C] to stop...&quot;&lt;/span&gt;

	&lt;span class=&quot;nb&quot;&gt;declare&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; i
	&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;i &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$INTERVAL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 0&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; i--&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
		&lt;/span&gt;tput cup 1 0
		&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Status: Next scan in &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$i&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; seconds      &quot;&lt;/span&gt;
		&lt;span class=&quot;nb&quot;&gt;sleep &lt;/span&gt;1
	&lt;span class=&quot;k&quot;&gt;done
done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;script-overview&quot;&gt;Script Overview&lt;/h2&gt;

&lt;p&gt;The script allows you to monitor the status of a list of servers, whether they’re URLs or hostnames, and provides real-time notifications when a server goes down. Here’s how it functions:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./server_status.sh &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &amp;lt;interval&amp;gt;] &amp;lt; server_list.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;key-features&quot;&gt;Key Features&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Flexible Input&lt;/strong&gt;: The script can read server names from a file or standard input.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Configurable Scan Interval&lt;/strong&gt;: You can specify a custom scan interval (default is 30 seconds).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Logging&lt;/strong&gt;: Automatically logs server down events to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/var/log/server_status.log&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Support for Both Hostnames and URLs&lt;/strong&gt;: Can ping traditional servers or check HTTP status codes for web servers.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;implementation&quot;&gt;Implementation&lt;/h2&gt;

&lt;h3 id=&quot;logging-setup&quot;&gt;Logging Setup&lt;/h3&gt;

&lt;p&gt;The script first checks if it can create and write to the log file at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/var/log/server_status.log&lt;/code&gt;. If the directory isn’t writable, it provides a helpful hint to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo&lt;/code&gt; when running the script for the first time. This ensures proper permissions are set for logging.&lt;/p&gt;

&lt;h3 id=&quot;server-status-checking&quot;&gt;Server Status Checking&lt;/h3&gt;

&lt;p&gt;The script supports two types of server checks:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# For traditional servers (hostnames)&lt;/span&gt;
ping &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; 1 &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;(Destination Host Unreachable|100% packet loss)&apos;&lt;/span&gt; &amp;amp;&amp;gt;/dev/null

&lt;span class=&quot;c&quot;&gt;# For web servers (URLs)&lt;/span&gt;
curl &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; /dev/null &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;%{http_code}&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;(000|404)&apos;&lt;/span&gt; &amp;amp;&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This dual approach allows monitoring of both network-level connectivity for traditional servers and HTTP availability for web services.&lt;/p&gt;

&lt;h3 id=&quot;user-experience&quot;&gt;User Experience&lt;/h3&gt;

&lt;p&gt;The script provides a clean, interactive terminal interface:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Colorized output (red for down servers)&lt;/li&gt;
  &lt;li&gt;Countdown timer to next scan&lt;/li&gt;
  &lt;li&gt;Option to stop monitoring with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CTRL+C&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;usage-example&quot;&gt;Usage Example&lt;/h3&gt;

&lt;p&gt;Create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server_list.txt&lt;/code&gt; with your servers:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com
https://mywebsite.com
192.168.1.100
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then run the script:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./server_status.sh &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; 60 &amp;lt; server_list.txt  &lt;span class=&quot;c&quot;&gt;# Scan every 60 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;considerations-and-warnings&quot;&gt;Considerations and Warnings&lt;/h2&gt;

&lt;p&gt;As with any system monitoring script, be cautious:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Ensure you have permission to monitor the servers&lt;/li&gt;
  &lt;li&gt;Be mindful of network load, especially with frequent scans&lt;/li&gt;
  &lt;li&gt;The script is best used in controlled, internal network environments&lt;/li&gt;
  &lt;li&gt;Unless the hostname starts with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http&lt;/code&gt;, the script assumes it’s a traditional server and uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ping&lt;/code&gt; for monitoring; therefore, it may not work for all types of servers if they don’t respond to ICMP requests (I had to add a custom rule to an AWS security group to allow ICMP traffic for this script to work for my EC2 instances)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;This bash script offers a lightweight, flexible solution for monitoring server availability. It demonstrates the power of bash scripting in system administration tasks, providing a simple yet effective tool for keeping an eye on your infrastructure.&lt;/p&gt;

&lt;p&gt;Remember to always test scripts in a staging environment before deploying to production, and customize the script to fit your specific monitoring needs.&lt;/p&gt;

&lt;p&gt;Happy scripting!&lt;/p&gt;
</description>
        <pubDate>Mon, 16 Dec 2024 00:00:00 +0000</pubDate>
        <link>https://nihilok.github.io//server-status-monitor-bash-script</link>
        <link href="https://nihilok.github.io/server-status-monitor-bash-script"/>
        <guid isPermaLink="true">https://nihilok.github.io/server-status-monitor-bash-script</guid>
      </item>
    
      <item>
        <title>Global find and replace with bash</title>
        <description>&lt;p&gt;I’m working on a server migration script (watch this space) where I need to do a global find and replace in a bunch of files on a VPS (Virtual Private Server). I’m currently reading a bbok called “Cybersecurity Ops with bash” by Paul Troncone, which has been a great resource for learning bash scripting, and I found a solution in the book that I can use to do the global find and replace:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;find /path/to/files &lt;span class=&quot;nt&quot;&gt;-type&lt;/span&gt; f &lt;span class=&quot;nt&quot;&gt;-exec&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;s/old-text/new-text/g&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{}&lt;/span&gt; +
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This command will find all standard files (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-type f&lt;/code&gt;) in the specified directory and replace all instances of “old-text” with “new-text”. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-i&lt;/code&gt; flag tells &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sed&lt;/code&gt; to edit the files in place, so be careful when using this command. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+&lt;/code&gt; at the end of the command is used to group the files together, which can improve performance when dealing with a large number of files.&lt;/p&gt;

&lt;p&gt;We can improve performance even further by using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xargs&lt;/code&gt; command:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;find /path/to/files &lt;span class=&quot;nt&quot;&gt;-type&lt;/span&gt; f &lt;span class=&quot;nt&quot;&gt;-print0&lt;/span&gt; | xargs &lt;span class=&quot;nt&quot;&gt;-0&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/old-text/new-text/g&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This command will also find all standard files in the specified directory, print them with a null character separator, and then pass them to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xargs&lt;/code&gt; which will run the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sed&lt;/code&gt; command on each file. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-0&lt;/code&gt; flag tells &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xargs&lt;/code&gt; to use the null character as the separator, which is useful for dealing with files that have spaces or special characters in their names.&lt;/p&gt;

&lt;p&gt;We can incorporate this command into a bash script to make it easier to use:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Check for the correct number of arguments&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$# &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-ne&lt;/span&gt; 3 &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Usage: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &amp;lt;directory&amp;gt; &amp;lt;old-text&amp;gt; &amp;lt;new-text&amp;gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
&lt;span class=&quot;k&quot;&gt;fi

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Replacing &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$2&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; with &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$3&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; in files in &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;...&quot;&lt;/span&gt;
find &lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-type&lt;/span&gt; f &lt;span class=&quot;nt&quot;&gt;-print0&lt;/span&gt; | xargs &lt;span class=&quot;nt&quot;&gt;-0&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$2&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$3&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/g&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Done.&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Save this script as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;global_replace.sh&lt;/code&gt; and make it executable with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chmod +x global_replace.sh&lt;/code&gt;. You can then use it like this:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./global_replace.sh /path/to/files &lt;span class=&quot;s2&quot;&gt;&quot;Some text&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Some other text&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will replace all instances of “Some text” with “Some other text” in the files in the specified directory which contained the text “Some text” (any other files will be ignored).&lt;/p&gt;

&lt;h2 id=&quot;do-we-even-need-to-use-find&quot;&gt;Do we even need to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find&lt;/code&gt;?&lt;/h2&gt;

&lt;p&gt;If you’re using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sed&lt;/code&gt; to replace text in files, you can use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sed&lt;/code&gt; command itself to specify the files you want to operate on. For example:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;s/old-text/new-text/g&apos;&lt;/span&gt; /path/to/files/&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This command will replace all instances of “old-text” with “new-text” in all files in the specified directory. You can also use wildcards to specify a subset of files. However, be careful when using wildcards, as they can match files you didn’t intend to operate on, including special files like sockets and symlinks. Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find&lt;/code&gt; is a safer way to target specific files.&lt;/p&gt;

&lt;p&gt;We could use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep&lt;/code&gt; to find the files we want to operate on, and then pass the list of files to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sed&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-lR&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;old-text&quot;&lt;/span&gt; /path/to/files | xargs &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;s/old-text/new-text/g&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This command will use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep&lt;/code&gt; to search for the text “old-text” in all files in the specified directory and its subdirectories, and then pass the list of files to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sed&lt;/code&gt; to replace the text. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-l&lt;/code&gt; flag tells &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep&lt;/code&gt; to only print the names of the files that contain the text, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-R&lt;/code&gt; flag tells &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep&lt;/code&gt; to search recursively through the directory. Again though, we need to be careful when using this command, as it can match files you didn’t intend to operate on.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;There are several ways to do a global find and replace in files using bash, but the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find&lt;/code&gt; command is a safe and reliable method of targeting specific files. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sed&lt;/code&gt; command is a powerful tool for text manipulation, and when combined with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep&lt;/code&gt;, it can be used to quickly and efficiently replace text in multiple files. Be careful when using these commands, especially when using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-i&lt;/code&gt; flag with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sed&lt;/code&gt;, as it will edit the files in place and can’t be undone. Always make sure to run commands in a testing/staging environment before using them in the wild, and/or back up your files before running any commands that modify them. Happy scripting!&lt;/p&gt;
</description>
        <pubDate>Sun, 15 Dec 2024 00:00:00 +0000</pubDate>
        <link>https://nihilok.github.io//global-find-and-replace-with-bash</link>
        <link href="https://nihilok.github.io/global-find-and-replace-with-bash"/>
        <guid isPermaLink="true">https://nihilok.github.io/global-find-and-replace-with-bash</guid>
      </item>
    
      <item>
        <title>Server setup script</title>
        <description>&lt;p&gt;Building on &lt;a href=&quot;/so-its-been-a-year&quot;&gt;a previous post&lt;/a&gt;, here is a script that automates the setup of a server. When setting up a  new server there are several things that I either do by default (on autopilot) or I forget to do. This script installs and configures various components such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nginx&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;certbot&lt;/code&gt; for Let’s Encrypt SSL certificates, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fail2ban&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ufw&lt;/code&gt;, and more. It also provides an option to disable password authentication and root login via SSH for added security. Here’s the script in full:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-euo&lt;/span&gt; pipefail

&lt;span class=&quot;nv&quot;&gt;IFS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;$&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\t&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Function to display error messages&lt;/span&gt;
error_exit&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Error: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
	&lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Ensure the script is run as root&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$EUID&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-ne&lt;/span&gt; 0 &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
	&lt;/span&gt;error_exit &lt;span class=&quot;s2&quot;&gt;&quot;This script must be run as root. Use or switch to the root user.&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Logging&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;LOG_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/var/log/server-setup.sh.log&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$LOG_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 2&amp;gt;&amp;amp;1

&lt;span class=&quot;c&quot;&gt;# Check which package manager is available and set the update and install commands&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-x&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; apt-get&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
	&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;PKG_UPDATE_CMD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;apt-get update&quot;&lt;/span&gt;
	&lt;span class=&quot;nv&quot;&gt;PKG_INSTALL_CMD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;apt-get install -y&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-x&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; apk&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
	&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;PKG_UPDATE_CMD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;apk update&quot;&lt;/span&gt;
	&lt;span class=&quot;nv&quot;&gt;PKG_INSTALL_CMD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;apk add&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-x&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; dnf&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
	&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;PKG_UPDATE_CMD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;dnf check-update&quot;&lt;/span&gt;
	&lt;span class=&quot;nv&quot;&gt;PKG_INSTALL_CMD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;dnf install -y&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-x&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; yum&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
	&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;PKG_UPDATE_CMD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;yum check-update&quot;&lt;/span&gt;
	&lt;span class=&quot;nv&quot;&gt;PKG_INSTALL_CMD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;yum install -y&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-x&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; pacman&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
	&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;PKG_UPDATE_CMD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;pacman -Sy&quot;&lt;/span&gt;
	&lt;span class=&quot;nv&quot;&gt;PKG_INSTALL_CMD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;pacman -S --noconfirm&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else
	&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Unsupported package manager&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
	&lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Check if system uses service or systemctl&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-x&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; systemctl&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
	function &lt;/span&gt;start &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
		systemctl start &lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;
		systemctl &lt;span class=&quot;nb&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;
	&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;restart &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
		systemctl restart &lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;
	&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-x&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; service&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
	function &lt;/span&gt;start &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
		service &lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt; start
		service &lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;enable&lt;/span&gt;
	&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;restart &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
		service &lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt; restart
	&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else
	&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Unsupported service manager&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
	&lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Update and upgrade&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Updating package repositories...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$PKG_UPDATE_CMD&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1

&lt;span class=&quot;c&quot;&gt;# Install packages&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Installing required packages...&quot;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$PKG_INSTALL_CMD&lt;/span&gt; git python3-dev python3-pip python3-venv python3-wheel
	&lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$PKG_INSTALL_CMD&lt;/span&gt; curl
	&lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$PKG_INSTALL_CMD&lt;/span&gt; nginx
	start nginx
	&lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$PKG_INSTALL_CMD&lt;/span&gt; certbot python3-certbot-nginx
	&lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$PKG_INSTALL_CMD&lt;/span&gt; sqlite3
	&lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$PKG_INSTALL_CMD&lt;/span&gt; fail2ban
	start fail2ban
	&lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$PKG_INSTALL_CMD&lt;/span&gt; ufw
	ufw allow &lt;span class=&quot;s1&quot;&gt;&apos;Nginx Full&apos;&lt;/span&gt;
	ufw allow &lt;span class=&quot;s1&quot;&gt;&apos;OpenSSH&apos;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1

&lt;span class=&quot;c&quot;&gt;# Ask user if they would like to enable the firewall&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Would you like to enable the firewall? (y/N): &quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; 1 &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$REPLY&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~ ^[Yy]&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
	&lt;/span&gt;ufw &lt;span class=&quot;nb&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;y
	&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Firewall enabled.&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else
	&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Firewall not enabled. Please enable it manually if needed.&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Create a new Nginx server block if the server name is provided and no existing configs are found&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;NGINX_SERVER_NAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;:-&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;NGINX_SERVER_CONFIG&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;
server {
    listen 80;
    server_name &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$NGINX_SERVER_NAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;;
    root /var/www/html;
    location / {
        try_files &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\$&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;uri &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\$&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;uri/ =404;
    }
}&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$NGINX_SERVER_NAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt;
	&lt;span class=&quot;c&quot;&gt;# Remove default Nginx server block if it exists&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /etc/nginx/sites-enabled/default &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
		&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Removing link to default nginx server block...&quot;&lt;/span&gt;
		&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; /etc/nginx/sites-enabled/default &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true
	&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

	&lt;span class=&quot;c&quot;&gt;# Create a new Nginx server block if no existing configs are found except the default&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /etc/nginx/sites-enabled/&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
		&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Creating a new Nginx server block...&quot;&lt;/span&gt;
		&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$NGINX_SERVER_CONFIG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/etc/nginx/sites-available/current
		&lt;span class=&quot;nb&quot;&gt;ln&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; /etc/nginx/sites-available/current /etc/nginx/sites-enabled/
		restart nginx
		&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Check/update the Nginx server block configuration at /etc/nginx/sites-available/current&quot;&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;else
		&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Existing Nginx server config found. No changes made.&quot;&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;fi
fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Ask user if they would like to disable password authentication&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Would you like to disable password authentication? (y/N): &quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; 1 &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$REPLY&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~ ^[Yy]&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
	&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Server setup complete!&quot;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Display warning and prompt for confirmation&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;************************************************************&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;WARNING: This script will now disable SSH password authentication&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;         and root login. Ensure you have SSH key-based access&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;         configured for all necessary user accounts.&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;         Disabling these settings without proper SSH keys&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;         can lock you out of the server.&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;************************************************************&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Do you want to proceed? (y/N): &quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; 1 &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Check user confirmation&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REPLY&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~ ^[Yy]&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
	&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Operation cancelled by the user.&quot;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Server setup complete!&quot;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0
&lt;span class=&quot;k&quot;&gt;fi

&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;SSHD_CONFIG&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/etc/ssh/sshd_config&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;BACKUP_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/etc/ssh/sshd_config.backup_&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; +%F_%T&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Backup the current SSH configuration&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SSHD_CONFIG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$BACKUP_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; error_exit &lt;span class=&quot;s2&quot;&gt;&quot;Failed to create backup of &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SSHD_CONFIG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Backup of sshd_config created at &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$BACKUP_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Function to update or add a configuration directive&lt;/span&gt;
update_config&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;local &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;directive&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;local &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$2&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

	&lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-q&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;^&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\s&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;*#&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\?\s&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;directive&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\s\+&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SSHD_CONFIG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt;
		&lt;span class=&quot;c&quot;&gt;# Uncomment and set the directive&lt;/span&gt;
		&lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s|^&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\s&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;*#&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\?\s&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;directive&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\s\+&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.*|&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;directive&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;|g&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SSHD_CONFIG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
		&lt;span class=&quot;c&quot;&gt;# Append the directive at the end of the file&lt;/span&gt;
		&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;directive&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SSHD_CONFIG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Disable password authentication&lt;/span&gt;
update_config &lt;span class=&quot;s2&quot;&gt;&quot;PasswordAuthentication&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;no&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Disable root login&lt;/span&gt;
update_config &lt;span class=&quot;s2&quot;&gt;&quot;PermitRootLogin&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;no&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Disable empty passwords for added security&lt;/span&gt;
update_config &lt;span class=&quot;s2&quot;&gt;&quot;PermitEmptyPasswords&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;no&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Disable challenge-response authentication&lt;/span&gt;
update_config &lt;span class=&quot;s2&quot;&gt;&quot;ChallengeResponseAuthentication&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;no&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Check SSH configuration syntax&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Checking SSH configuration syntax...&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; sshd &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
	&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;SSH configuration syntax is invalid. Restoring the original configuration.&quot;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$BACKUP_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SSHD_CONFIG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; error_exit &lt;span class=&quot;s2&quot;&gt;&quot;Failed to restore the original sshd_config.&quot;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Function to restart SSH service&lt;/span&gt;
restart_sshd&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;local &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;sshd&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ssh&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;service &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[@]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
		if &lt;/span&gt;systemctl list-units &lt;span class=&quot;nt&quot;&gt;--type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;service | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-q&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.service&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
			&lt;/span&gt;systemctl restart &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$service&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return &lt;/span&gt;0
		&lt;span class=&quot;k&quot;&gt;elif &lt;/span&gt;service &lt;span class=&quot;nt&quot;&gt;--status-all&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-q&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$service&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
			&lt;/span&gt;service &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$service&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; restart &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return &lt;/span&gt;0
		&lt;span class=&quot;k&quot;&gt;fi
	done
	return &lt;/span&gt;1
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Prompt to restart SSH service&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Do you want to restart the SSH service now? (y/N): &quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; 1 &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REPLY&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~ ^[Yy]&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
	&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Restarting SSH service...&quot;&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; restart_sshd&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
		&lt;/span&gt;error_exit &lt;span class=&quot;s2&quot;&gt;&quot;Failed to restart SSH service. Please restart it manually.&quot;&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;fi
else
	&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Please restart the SSH service manually when ready.&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fi

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;SSH password authentication and root login have been successfully disabled.&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Server setup complete! Please verify that you can log in using SSH keys before closing your current session.&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo
exit &lt;/span&gt;0

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This script can be downloaded from &lt;a href=&quot;https://gist.github.com/nihilok/d57ce168bc643e2ac18c0b54a7877aba&quot;&gt;here&lt;/a&gt; and run on a server with the following commands:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl https://gist.githubusercontent.com/nihilok/d57ce168bc643e2ac18c0b54a7877aba/raw/server-setup.sh &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; server-setup.sh
&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x server-setup.sh
&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; ./server-setup.sh example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can omit the server name arg (example.com) if you don’t want to create an Nginx server block. The script will install and configure the other components without creating an Nginx server block.&lt;/p&gt;
</description>
        <pubDate>Sat, 14 Dec 2024 00:00:00 +0000</pubDate>
        <link>https://nihilok.github.io//server-setup-script</link>
        <link href="https://nihilok.github.io/server-setup-script"/>
        <guid isPermaLink="true">https://nihilok.github.io/server-setup-script</guid>
      </item>
    
      <item>
        <title>Detecting a Loop</title>
        <description>&lt;p&gt;OpenAI’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;o1-mini&lt;/code&gt; (coding/reasoning) model had this to say about detecting a loop in a 2D grid:&lt;/p&gt;

&lt;p&gt;To determine whether there’s a possible looping path in a 2D grid of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MapPosition&lt;/code&gt; objects—where the loop can be any closed path (not necessarily a perfect rectangle or square) and may include branches or paths outside the loop—you’ll need to perform a more generalized cycle detection within the grid.&lt;/p&gt;

&lt;h3 id=&quot;problem-clarification&quot;&gt;&lt;strong&gt;Problem Clarification&lt;/strong&gt;&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Loop Definition:&lt;/strong&gt; A loop is a closed path where you can start at a cell and return to it by traversing adjacent cells (horizontally or vertically) without retracing your steps. For the loop to be valid:
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Each cell in the loop must have been visited at least twice&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_times_visited &amp;gt;= 2&lt;/code&gt;), indicating that the path has entered and exited each cell in the loop.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Additional Paths/Bananas:&lt;/strong&gt; There can be branches or paths outside the loop that might have cells visited fewer times, but these should not affect the existence of the loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;approach-overview&quot;&gt;&lt;strong&gt;Approach Overview&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;To solve this, we’ll treat the grid as a graph where each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MapPosition&lt;/code&gt; is a node connected to its adjacent nodes (up, down, left, right). We are interested in finding &lt;strong&gt;any cycle&lt;/strong&gt; in this graph where:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;All nodes in the cycle have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_times_visited &amp;gt;= 2&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This way, the cycle represents a looped path that has been traversed at least twice, satisfying the condition for a looping path.&lt;/p&gt;

&lt;h3 id=&quot;cycle-detection-using-depth-first-search-dfs&quot;&gt;&lt;strong&gt;Cycle Detection Using Depth-First Search (DFS)&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;We’ll implement a &lt;strong&gt;Depth-First Search (DFS)&lt;/strong&gt; algorithm tailored for cycle detection in undirected graphs. Here’s how:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Graph Representation:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Consider each cell with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_times_visited &amp;gt;= 2&lt;/code&gt; as a node.&lt;/li&gt;
      &lt;li&gt;Connect nodes to their adjacent (up, down, left, right) neighbors that also have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_times_visited &amp;gt;= 2&lt;/code&gt;.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;DFS Traversal:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Traverse the graph using DFS.&lt;/li&gt;
      &lt;li&gt;Keep track of visited nodes to avoid redundant processing.&lt;/li&gt;
      &lt;li&gt;During traversal, if we encounter an already visited node that’s not the immediate parent, a cycle exists.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Handling Grid Boundaries:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Ensure that adjacency checks respect the grid’s boundaries to avoid index errors.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;implementation&quot;&gt;&lt;strong&gt;Implementation&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;Here’s the Python implementation adhering to the above approach:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;dataclasses&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dataclass&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;typing&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Tuple&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dataclass&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MapPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;num_times_visited&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;has_looping_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MapPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]])&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cols&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;visited&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[[&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cols&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;is_valid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cols&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_times_visited&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;dfs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent_y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent_x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;visited&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# Define the four possible directions: up, down, left, right
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;directions&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dx&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;directions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;ny&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dx&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;is_valid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ny&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Skip invalid or unqualified cells
&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visited&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ny&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dfs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ny&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ny&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent_y&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent_x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;c1&quot;&gt;# Found a neighbor that&apos;s visited and not the parent =&amp;gt; cycle
&lt;/span&gt;                &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cols&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_times_visited&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visited&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dfs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;explanation&quot;&gt;&lt;strong&gt;Explanation&lt;/strong&gt;&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Initial Checks:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;The function first checks if the grid is empty. If so, it returns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt; since no loops can exist.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Visited Matrix:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;A 2D &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;visited&lt;/code&gt; matrix keeps track of which cells have been processed during DFS to prevent redundant traversals.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Helper Functions:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;is_valid(y, x)&lt;/code&gt;: Checks if the cell &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(y, x)&lt;/code&gt; is within grid boundaries and has been visited at least twice (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_times_visited &amp;gt;= 2&lt;/code&gt;).&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Depth-First Search (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dfs&lt;/code&gt;):&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Marks the current cell as visited.&lt;/li&gt;
      &lt;li&gt;Iterates through all four possible adjacent directions (up, down, left, right).&lt;/li&gt;
      &lt;li&gt;For each valid neighbor:
        &lt;ul&gt;
          &lt;li&gt;If the neighbor hasn’t been visited, recursively perform DFS on it.&lt;/li&gt;
          &lt;li&gt;If the neighbor has been visited and is &lt;strong&gt;not&lt;/strong&gt; the parent from which we arrived, a cycle is detected, and the function returns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt;.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Main Loop:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Iterates through every cell in the grid.&lt;/li&gt;
      &lt;li&gt;For each cell that qualifies (i.e., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_times_visited &amp;gt;= 2&lt;/code&gt; and not yet visited), it initiates a DFS.&lt;/li&gt;
      &lt;li&gt;If any DFS traversal finds a cycle, the function immediately returns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt;.&lt;/li&gt;
      &lt;li&gt;If no cycles are found after exploring all qualifying cells, the function returns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt;.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;time-and-space-complexity&quot;&gt;&lt;strong&gt;Time and Space Complexity&lt;/strong&gt;&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Time Complexity:&lt;/strong&gt; O(rows * cols), since each cell is visited at most once.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Space Complexity:&lt;/strong&gt; O(rows * cols) for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;visited&lt;/code&gt; matrix and recursion stack in the worst case.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;example-usage&quot;&gt;&lt;strong&gt;Example Usage&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;Here’s how you might use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;has_looping_path&lt;/code&gt; function with a sample grid:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Example grid:
# A 3x3 grid where the outer perimeter forms a loop visited twice,
# and the inner cell is visited once.
&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MapPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_times_visited&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MapPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_times_visited&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MapPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_times_visited&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)],&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MapPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_times_visited&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MapPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_times_visited&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MapPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_times_visited&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)],&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MapPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_times_visited&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MapPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_times_visited&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MapPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_times_visited&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;has_looping_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Output: True
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this example, the outer perimeter of the grid forms a cycle (loop) where each perimeter cell has been visited at least twice (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_times_visited &amp;gt;= 2&lt;/code&gt;). The inner cell &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(1,1)&lt;/code&gt; is visited only once and does not affect the existence of the external loop. Hence, the function returns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;alternative-detecting-rectangularsquare-loops&quot;&gt;&lt;strong&gt;Alternative: Detecting Rectangular/Square Loops&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;If your specific application requires detecting only rectangular or square loops (as opposed to any arbitrary cycle), the initial approach proposed can be utilized. Here’s a refined version focusing on rectangular/square loops with early termination for efficiency:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;has_rectangular_loop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MapPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]])&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cols&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Precompute visited cells
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;visited_cells&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cols&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_times_visited&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;visited_cells&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Iterate through all possible rectangles
&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y1&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x1&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cols&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visited_cells&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Skip cells not visited enough
&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y2&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x2&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cols&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
                    &lt;span class=&quot;c1&quot;&gt;# Check if all perimeter cells of the rectangle are in visited_cells
&lt;/span&gt;                    &lt;span class=&quot;n&quot;&gt;is_loop&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;

                    &lt;span class=&quot;c1&quot;&gt;# Top and Bottom edges
&lt;/span&gt;                    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
                        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visited_cells&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visited_cells&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                            &lt;span class=&quot;n&quot;&gt;is_loop&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
                            &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;

                    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;is_loop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                        &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;

                    &lt;span class=&quot;c1&quot;&gt;# Left and Right edges
&lt;/span&gt;                    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
                        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visited_cells&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visited_cells&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                            &lt;span class=&quot;n&quot;&gt;is_loop&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
                            &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;

                    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;is_loop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Found a valid rectangular loop
&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This function specifically checks for rectangular or square loops where all perimeter cells have been visited at least twice. It avoids unnecessary checks by precomputing visited cells, which can offer some performance benefits over the initial assistant’s approach.&lt;/p&gt;

&lt;h3 id=&quot;conclusion&quot;&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;The generalized DFS-based cycle detection approach is more versatile and efficient for detecting any form of loop within the grid, regardless of its shape. However, if your application strictly requires detecting rectangular or square loops, the alternative method provided can be employed.&lt;/p&gt;

&lt;p&gt;Choose the method that best aligns with your specific requirements:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;General Loop Detection:&lt;/strong&gt; Use the DFS-based &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;has_looping_path&lt;/code&gt; function.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Rectangular/Square Loop Detection:&lt;/strong&gt; Use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;has_rectangular_loop&lt;/code&gt; function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both methods ensure that only loops where each involved cell has been visited at least twice are considered valid.&lt;/p&gt;
</description>
        <pubDate>Tue, 10 Dec 2024 00:00:00 +0000</pubDate>
        <link>https://nihilok.github.io//detecting-a-loop</link>
        <link href="https://nihilok.github.io/detecting-a-loop"/>
        <guid isPermaLink="true">https://nihilok.github.io/detecting-a-loop</guid>
      </item>
    
  </channel>
</rss>
