<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jonne Riepponen</title>
    <description>The latest articles on DEV Community by Jonne Riepponen (@jronegit).</description>
    <link>https://dev.to/jronegit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4163710%2F26ffbc3d-0094-44d1-9f8b-30737c1f6aa4.png</url>
      <title>DEV Community: Jonne Riepponen</title>
      <link>https://dev.to/jronegit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jronegit"/>
    <language>en</language>
    <item>
      <title>Local LLMs as System Tooling: Running Sub-3B Models on Edge and Budget Hardware</title>
      <dc:creator>Jonne Riepponen</dc:creator>
      <pubDate>Mon, 02 Nov 2026 18:34:38 +0000</pubDate>
      <link>https://dev.to/jronegit/local-llms-as-system-tooling-running-sub-3b-models-on-edge-and-budget-hardware-50c5</link>
      <guid>https://dev.to/jronegit/local-llms-as-system-tooling-running-sub-3b-models-on-edge-and-budget-hardware-50c5</guid>
      <description>&lt;p&gt;You need an AI assistant that runs on your own machine. The cloud options are good but they are not yours. You want something that actually &lt;em&gt;does things&lt;/em&gt; — reads your screen, controls your smart home, answers questions in Finnish or English, and never sends your data to a server you don't control.&lt;/p&gt;

&lt;p&gt;The local LLM trend is about as practical as it sounds. It works on mid-range hardware if you pick the right model size. A sub-3B parameter model fits comfortably on consumer gear and handles everyday tasks without needing a dedicated GPU or a rack of servers. The trade-off versus cloud options is that the response times are slower, but your parents' conversations do not become training data for a future model owned by a third party.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "local" matters more than "smarter"
&lt;/h2&gt;

&lt;p&gt;The most capable AI in 2026 is still a cloud-hosted model from OpenAI or Anthropic. But "most capable" is the wrong axis for elderly parents. They are not pushing the model with complex prompts or asking it to write production code. They are asking it to summarize a long email, explain a confusing medication instruction, draft a birthday reply to a grandchild, or settle a trivia dispute.&lt;/p&gt;

&lt;p&gt;For all of those tasks, a smaller, more private model is more than sufficient — and the privacy trade-off is meaningfully better. The three options below all share one property: your parents' conversations do not become training data for a future model owned by a third party. That single property is worth more than a few IQ points of model quality.&lt;/p&gt;

&lt;p&gt;You might think you need the biggest GPU to run this locally. You don't. A sub-3B model runs on an older laptop or a budget desktop. The key feature here is function calling — the model does not execute commands directly but returns a structured JSON decision that your script interprets. This is cleaner than giving the LLM raw bash access. The function list is a strict allowlist — defined in JSON Schema. The model can only call what is explicitly permitted. No &lt;code&gt;rm -rf /&lt;/code&gt;, no matter how creatively phrased.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1 — Run Ollama on an old machine
&lt;/h2&gt;

&lt;p&gt;If your parents have an older Mac, a Windows PC from the last few years, or a Linux box gathering dust, &lt;strong&gt;Ollama&lt;/strong&gt; is the strongest default choice. It is built into the system via a simple binary install, requires no separate app store login, and does not require a separate cloud account for the model itself. Your parents do not have to "use" Ollama — they just continue using their devices as they always have, and the AI shows up where it is useful: inside a chat window or a smart home dashboard.&lt;/p&gt;

&lt;p&gt;The privacy architecture is genuinely best-in-class for consumer AI when you host it yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On-device first.&lt;/strong&gt; Most requests are processed locally on the device's CPU or integrated GPU. The data never leaves the machine unless you explicitly configure an API endpoint to send it elsewhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private Cloud Compute (optional).&lt;/strong&gt; For requests that need more compute than the phone can provide, you can route them to dedicated servers, but this is not required for sub-3B models. They run fine on a CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No training on personal data.&lt;/strong&gt; You are the model owner. Your parents' Ollama requests do not get sent to OpenAI or Anthropic. This is an architectural commitment, not just a setting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The catch: you must manage the updates. If your machine does not have access to the internet, the model binaries will not update automatically. You need to check for new releases and pull them manually. If your parents are in a supported region for Apple Intelligence, you might still prefer that option over Ollama for simplicity. Check Settings → General → Apple Intelligence &amp;amp; Siri on their device; if the menu is present and the toggle works, they are eligible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup time:&lt;/strong&gt; 10 minutes on macOS or Linux, 20 minutes on Windows. Run the installer script or download the binary from the official site. Open a terminal or command prompt. Type &lt;code&gt;ollama run qwen2.5:3b&lt;/code&gt;. Wait for the pull to finish. Talk them through three demo tasks: "summarize this notification stack," "rewrite this email to be shorter," and "create a fun image of a cat." That is enough for them to internalize what the AI can do without overwhelming them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2 — Use a browser-based local runner
&lt;/h2&gt;

&lt;p&gt;If your parents are on Android, on a Windows PC, or simply do not have a recent enough machine for Apple Intelligence, the next best default is &lt;strong&gt;KoboldCPP&lt;/strong&gt; or &lt;strong&gt;LM Studio&lt;/strong&gt;, used through a browser at &lt;code&gt;localhost:5001&lt;/code&gt;. These tools run locally but offer a web interface that looks familiar to anyone who has used a chatbot before.&lt;/p&gt;

&lt;p&gt;KoboldCPP's privacy posture for local users is stronger than most cloud tools: you run the binary yourself, and it does not train on user conversations by default, full stop, on any tier. There is no setting to flip — the protection is built in because the data never leaves your machine. For elderly users who do not care which logo is on the page, KoboldCPP is currently the more privacy-respecting default choice for Windows machines with 8GB of RAM or more.&lt;/p&gt;

&lt;p&gt;The trade-off: local runners on older hardware have slower response times than cloud services. You will see "processing" indicators for a few seconds longer. For someone using AI a few times a week, this is fine. For someone who has integrated it into daily routines, it is annoying but not dangerous. You can set the temperature and context window to match their needs. Lower temperature means more factual answers; higher temperature means more creative writing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;RAM needed&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;td&gt;macOS/Linux/Windows&lt;/td&gt;
&lt;td&gt;6GB+&lt;/td&gt;
&lt;td&gt;System integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LM Studio&lt;/td&gt;
&lt;td&gt;Windows/macOS/Linux&lt;/td&gt;
&lt;td&gt;4GB+&lt;/td&gt;
&lt;td&gt;Easy web UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KoboldCPP&lt;/td&gt;
&lt;td&gt;Windows/Linux&lt;/td&gt;
&lt;td&gt;4GB+&lt;/td&gt;
&lt;td&gt;High compatibility&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All fit comfortably on mid-range hardware or run on CPU. Your Intel i5 desktop handles it fine if you pick the right model. The models I'm using are intentionally small: &lt;code&gt;qwen2.5:3b&lt;/code&gt; for the main orchestrator, &lt;code&gt;llama3.2:3b&lt;/code&gt; as a fallback, and &lt;code&gt;phi3:mini&lt;/code&gt; for lightweight tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The System Agent — Desktop Control
&lt;/h2&gt;

&lt;p&gt;Once you have Ollama or LM Studio running locally, you can build a simple agent to control the desktop environment. Things it can do include setting volume with &lt;code&gt;pamixer&lt;/code&gt;, adjusting brightness with &lt;code&gt;xbacklight&lt;/code&gt;, launching applications with &lt;code&gt;xdg-open&lt;/code&gt;, and getting active window info via &lt;code&gt;wmctrl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No raw bash. Every function is explicitly defined in a JSON schema file that the model reads before taking an action. The model sees a list of allowed functions and can only call what's on that list. This prevents the "I want to delete all my files" prompt from accidentally deleting anything, because the command to do so is not on the allowlist.&lt;/p&gt;

&lt;p&gt;You might wonder if this setup is safe for your parents. If they are comfortable using their phones without fear of breaking something, they can use a local LLM agent with similar confidence. The only real risk is accidental deletion if the function list is too broad. Keep it strict. Define specific functions like &lt;code&gt;set_volume(level)&lt;/code&gt; or &lt;code&gt;open_app(name)&lt;/code&gt;. Do not add &lt;code&gt;delete_file()&lt;/code&gt; unless you really want to risk losing data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vision Agent — "The Eyes"
&lt;/h2&gt;

&lt;p&gt;If your parents have a tablet with a camera, you can take a screenshot or read a camera stream, then run the image through a lightweight VLM like &lt;code&gt;Moondream2&lt;/code&gt;. It can answer: &lt;em&gt;"What window is in the foreground?"&lt;/em&gt;, &lt;em&gt;"Did that build succeed?"&lt;/em&gt;, &lt;em&gt;"What's on my second monitor right now?"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The setup requires a tool to capture the screen image before sending it to the model. On Linux, use &lt;code&gt;maim&lt;/code&gt; or &lt;code&gt;grim&lt;/code&gt;. On Windows, use &lt;code&gt;PowerShell&lt;/code&gt; commands with the built-in clipboard support. The model then analyzes the image and returns text. You can chain this with the system agent to read screen content aloud or highlight important windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Home Assistant Agent
&lt;/h2&gt;

&lt;p&gt;If your parents have smart home devices, you can talk to a Home Assistant instance over its WebSocket API. Lights, switches, climate, sensors — all accessible via the local network. I can say: &lt;em&gt;"Turn off the office lights, but only if nobody's in there"&lt;/em&gt; and it checks the occupancy sensor before acting.&lt;/p&gt;

&lt;p&gt;This setup requires the Home Assistant container to expose its API endpoint to the local LLM agent. The model sends a message like "dim the living room light" and the agent parses it into a JSON call to the Home Assistant API. The agent handles the HTTP request and returns success or failure status to the user interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Media &amp;amp; Data Agent
&lt;/h2&gt;

&lt;p&gt;You might want the AI to pull in external data when needed: weather forecasts, electricity prices relevant in Finland, or web searches via a headless browser tool like &lt;code&gt;curl&lt;/code&gt; or &lt;code&gt;playwright&lt;/code&gt;. This is optional but useful if your parents need to plan their day around energy costs or check the weather for outdoor activities.&lt;/p&gt;

&lt;p&gt;The agent runs in its own Docker container and does not know about the other agents. It just exposes a clean REST interface and waits for instructions from Ollama. Each agent is a &lt;strong&gt;FastAPI service&lt;/strong&gt; inside a Docker container. They don't know about each other. They just expose a clean REST interface and wait for instructions from Ollama.&lt;/p&gt;

&lt;p&gt;This isolation prevents one agent from breaking another. If the media agent crashes, the system agent keeps working. You can restart containers individually without affecting the whole stack. This is better than running everything as one monolithic Python script.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff — Local vs Cloud
&lt;/h2&gt;

&lt;p&gt;You might ask why not just use the cloud service. The answer is simple: privacy. When you run locally, your parents' conversations do not become training data for a future model owned by a third party. That single property is worth more than a few IQ points of model quality.&lt;/p&gt;

&lt;p&gt;The local runner does not have unlimited context windows or infinite message quotas. You will hit hardware limits sooner than cloud services. A sub-3B model on a budget machine will choke on long documents that fit easily in the cloud. This is fine for most users who just want to summarize emails or draft messages. If you need more power, you can switch to a larger model or upgrade the hardware later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup checklist
&lt;/h2&gt;

&lt;p&gt;Here is what you should do next time you visit your parents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install Ollama or LM Studio on their machine.&lt;/li&gt;
&lt;li&gt;Pull a sub-3B model like &lt;code&gt;qwen2.5:3b&lt;/code&gt; for general tasks.&lt;/li&gt;
&lt;li&gt;Configure a simple agent to control volume, brightness, and app launching.&lt;/li&gt;
&lt;li&gt;Set up a web interface so they can chat without opening a terminal.&lt;/li&gt;
&lt;li&gt;Test three demo tasks together to show what the AI can do.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are unsure about Docker or the command line, stick to the installer scripts. They handle most of the configuration for you. You just type &lt;code&gt;ollama run qwen2.5:3b&lt;/code&gt; and wait for it to finish downloading. The model will start automatically after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;You do not need a new computer to run local AI. An old laptop or a budget desktop works fine for sub-3B models. The setup takes ten minutes on Linux, twenty on Windows, and two on macOS. Your parents' conversations stay private because they never leave the machine.&lt;/p&gt;

&lt;p&gt;Whatever you choose, take 30 minutes on a Saturday to set it up properly. The local LLM is the second-most-important thing; the setup is the first. A senior with a well-set-up local AI will be happier than a senior with a poorly-set-up flagship cloud service.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>localllm</category>
      <category>privacy</category>
      <category>seniortech</category>
    </item>
    <item>
      <title>I Built a Password Strength Checker That Runs 100% in Your Browser (and What It Taught Me About Passwords)</title>
      <dc:creator>Jonne Riepponen</dc:creator>
      <pubDate>Wed, 07 Oct 2026 15:38:39 +0000</pubDate>
      <link>https://dev.to/jronegit/i-built-a-password-strength-checker-that-runs-100-in-your-browser-and-what-it-taught-me-about-2oka</link>
      <guid>https://dev.to/jronegit/i-built-a-password-strength-checker-that-runs-100-in-your-browser-and-what-it-taught-me-about-2oka</guid>
      <description>&lt;h2&gt;
  
  
  The Phone Call That Started This
&lt;/h2&gt;

&lt;p&gt;A client's junior admin called me last month. Their company had just rolled out a new password policy — 14 characters, three character classes, rotation every 90 days — and the helpdesk was drowning in reset tickets. The admin was proud of the policy. The users were writing the new passwords on sticky notes stuck to their monitors.&lt;/p&gt;

&lt;p&gt;That is the entire password industry in one story: we keep making the rules harder without making the passwords better.&lt;/p&gt;

&lt;p&gt;So I did what I always do when I'm annoyed: I built the thing I wish existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tool: 100% Client-Side, Nothing Leaves Your Browser
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pragmaticsysadmin.help/tools/password-checker.html" rel="noopener noreferrer"&gt;Try the Password Strength Checker&lt;/a&gt; — paste a password, get an honest strength rating: length, character classes, and common-pattern weaknesses. No account, no tracking, no server. Open the network tab in dev tools if you don't believe me; nothing is sent anywhere.&lt;/p&gt;

&lt;p&gt;Why does that matter? Because a non-trivial number of online "password checkers" submit what you type to their backend for "analysis." Think about that for a second. You are sending your &lt;em&gt;actual password&lt;/em&gt; to a stranger's server to find out if it's good. I built mine specifically so that can't happen — there is no backend. The HTML file doesn't even make a fetch request.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Learned Testing 200 Passwords
&lt;/h2&gt;

&lt;p&gt;I ran a pile of real-world-style passwords through the checker while building it — the kind of passwords people actually use, not the kind security training pretends they use. Three findings surprised me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Length beats cleverness, every time.&lt;/strong&gt; A 20-character passphrase of plain words (&lt;code&gt;correct horse battery staple&lt;/code&gt; style) outscores an 8-character symbol-soup password on every honest metric. The math isn't close: each extra character multiplies the search space, while swapping &lt;code&gt;e&lt;/code&gt; for &lt;code&gt;3&lt;/code&gt; adds almost nothing because attackers already try that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The complexity rules actively hurt.&lt;/strong&gt; Forcing symbols and numbers pushes people toward predictable patterns: &lt;code&gt;Password1!&lt;/code&gt;, &lt;code&gt;Summer2025!&lt;/code&gt;, company-name-plus-year. Attackers' rule lists contain exactly these mutations. A policy that bans those patterns would be ten times more effective than a policy that demands a symbol — but "must contain !" is easier to audit than "must not be predictable," so here we are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Rotation policies create the problem they claim to solve.&lt;/strong&gt; Every 90-day rotation study I've read says the same thing: users increment a number (&lt;code&gt;...Q3&lt;/code&gt; → &lt;code&gt;...Q4&lt;/code&gt;) or rotate through three passwords. The new password is never stronger; it's just newer. If a password is strong and unique, leave it alone. Rotate on &lt;em&gt;suspicion of compromise&lt;/em&gt;, not on a calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  What To Do Instead (The Pragmatic Version)
&lt;/h2&gt;

&lt;p&gt;For yourself and your users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use a password manager.&lt;/strong&gt; Bitwarden is free and open source. This is the single highest-leverage security change a normal person can make, and I wrote a &lt;a href="https://pragmaticsysadmin.help/senior-tech/2026-07-27-password-manager-for-elderly-parents/" rel="noopener noreferrer"&gt;10-minute setup guide for elderly parents&lt;/a&gt; that works for anyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One long, unique password per service&lt;/strong&gt; — generated, never reused. Reuse is what actually gets accounts taken over, not weak passwords.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn on 2FA everywhere that matters&lt;/strong&gt;, especially email — email is the master key to every "forgot password" flow you have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test your own passwords&lt;/strong&gt; with the checker. If your scheme produces anything the tool flags as a common pattern, change the scheme, not the password.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Build Notes (For Fellow Tinkerers)
&lt;/h2&gt;

&lt;p&gt;The whole tool is one self-contained HTML file: vanilla JS, no dependencies, no build step. The scoring is deliberately simple and explainable — entropy estimate from length and character classes, minus penalties for dictionary words, keyboard walks (&lt;code&gt;qwerty&lt;/code&gt;, &lt;code&gt;1qaz&lt;/code&gt;), repeated characters, and date/sequence patterns. No machine learning, no API calls, no analytics.&lt;/p&gt;

&lt;p&gt;Simplicity is the point. A security tool you can't audit is a security tool you shouldn't trust, and "trust me, it runs locally" is a claim anyone can verify in the network tab.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Try it: &lt;a href="https://pragmaticsysadmin.help/tools/password-checker.html" rel="noopener noreferrer"&gt;Password Strength Checker&lt;/a&gt; — free, private, no account. If it saves one person from sending their password to a stranger's server, it was worth the afternoon.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sysadmin</category>
    </item>
    <item>
      <title>A Sandbox for the Linux Commands You're Afraid to Run</title>
      <dc:creator>Jonne Riepponen</dc:creator>
      <pubDate>Mon, 05 Oct 2026 11:55:46 +0000</pubDate>
      <link>https://dev.to/jronegit/a-sandbox-for-the-linux-commands-youre-afraid-to-run-4n3h</link>
      <guid>https://dev.to/jronegit/a-sandbox-for-the-linux-commands-youre-afraid-to-run-4n3h</guid>
      <description>&lt;p&gt;Almost nobody learns &lt;code&gt;rm -rf&lt;/code&gt; the easy way. You read the man page, you use it carefully for a year, and then one day you watch your finger hit Enter a fraction of a second before your brain finishes checking the path.&lt;/p&gt;

&lt;p&gt;The commands that destroy data don't punish typos. They punish the half second before the typo, when you were certain the path was right.&lt;/p&gt;

&lt;p&gt;That half second of doubt isn't something documentation can give you. So I built a place where you can get it wrong on purpose, with nothing at stake but a score.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fake shell that keeps score
&lt;/h2&gt;

&lt;p&gt;The whole thing is one HTML file. No build step, no npm, no CDN — 2,340 lines, about 84 KB, and it runs from a local file if you want it to. You can &lt;a href="https://pragmaticsysadmin.help/tools/command-simulator.html" rel="noopener noreferrer"&gt;try it here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The prompt reads &lt;code&gt;sysadmin@danger-zone:~$&lt;/code&gt;, and the &lt;code&gt;~&lt;/code&gt; isn't decoration. Under the surface there's a small state object holding the entire world:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;currentDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/home/sysadmin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dangerLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LOW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;commandsUsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;safetyScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;currentFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seventeen commands are wired up. &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cd&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;df&lt;/code&gt; and &lt;code&gt;ps&lt;/code&gt; handle the boring work, so you can navigate a fake filesystem that behaves the way you expect. The other five are the reason the tool exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every dangerous command costs you twenty points
&lt;/h2&gt;

&lt;p&gt;There is no confirmation prompt anywhere in it. That's on purpose. A real shell doesn't stop to ask, so a simulator that stops to ask teaches you the wrong reflex.&lt;/p&gt;

&lt;p&gt;You pay in score instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dangerous&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;triggerDangerEffects&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;safetyScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;safetyScore&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;updateStats&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twenty points per mistake, starting from a clean 100. The danger meter recalculates on every keystroke: 80 and above is LOW, 60 is MEDIUM, 40 is HIGH, and anything under that is CRITICAL.&lt;/p&gt;

&lt;p&gt;Five mistakes puts you at zero. That number isn't a punishment. It's a count of how many times you would have needed a restore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The matcher reads tokens, not a real shell
&lt;/h2&gt;

&lt;p&gt;Each dangerous handler inspects the argument array for the exact tokens that make the command lethal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rmCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-rf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/etc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Files would be deleted in a real system.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;In simulation: No actual files were harmed.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dangerous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;File deletion simulated safely.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dangerous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dd if=/dev/zero of=/dev/sda&lt;/code&gt; triggers an explosion animation. &lt;code&gt;chmod 777 /etc/passwd&lt;/code&gt; and &lt;code&gt;iptables -F&lt;/code&gt; raise their own security warnings. &lt;code&gt;mkfs /dev/sda1&lt;/code&gt; formats nothing and tells you so.&lt;/p&gt;

&lt;p&gt;It's a token check, not a parser. That distinction turns out to matter more than it sounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;rm -rf /*&lt;/code&gt; walks straight through
&lt;/h2&gt;

&lt;p&gt;Here's the hole, and I'd rather you hear it from me than find it yourself.&lt;/p&gt;

&lt;p&gt;The check asks whether &lt;code&gt;args&lt;/code&gt; contains the literal string &lt;code&gt;/&lt;/code&gt;. The command that has actually ended careers is &lt;code&gt;rm -rf /*&lt;/code&gt;, and after splitting on spaces its arguments are &lt;code&gt;['-rf', '/*']&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;'/*'&lt;/code&gt; is not &lt;code&gt;'/'&lt;/code&gt;. No match, no warning, no points deducted. The tool congratulates you: &lt;em&gt;File deletion simulated safely.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Same story with &lt;code&gt;rm -rf ~&lt;/code&gt;, &lt;code&gt;rm -rf $HOME&lt;/code&gt; and &lt;code&gt;rm -rf .&lt;/code&gt;. I matched the memorable version of the footgun and missed the common one. If you want to fix that, the honest version is to test each token with a prefix check, not an equality check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isRootish&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[/&lt;/span&gt;&lt;span class="sr"&gt;~.&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's four lines and it catches the cases I shipped without. I'd rather show you the gap than let the tool quietly lie to you about your own reflexes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two handlers I wrote and could never reach
&lt;/h2&gt;

&lt;p&gt;The dispatcher splits the line on spaces, then switches on the first token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;catCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rmCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two cases, though, were written as whole commands with spaces still in them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cat /dev/random&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                 &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomCommand&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;echo 1 &amp;gt; /proc/sys/kernel/panic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;panicCommand&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cmd&lt;/code&gt; is &lt;code&gt;parts[0]&lt;/code&gt;. It can never contain a space, so neither case can ever fire. &lt;code&gt;cat /dev/random&lt;/code&gt; falls through to the ordinary &lt;code&gt;cat&lt;/code&gt; branch and gets quietly ignored.&lt;/p&gt;

&lt;p&gt;I found that while going back through the file to write this post. It's the honest argument for writing build logs: the bug had been sitting there since the day I wrote it, and explaining the code out loud is what finally made me look.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually protects your data
&lt;/h2&gt;

&lt;p&gt;A sandbox builds the reflex. It does not protect anything. Before you run anything recursive, work through this list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dry-run it.&lt;/strong&gt; &lt;code&gt;rsync -av --dry-run&lt;/code&gt; and &lt;code&gt;find . -name '*.log' -print&lt;/code&gt; list what would be touched. Read the list properly instead of skimming it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Print the variable.&lt;/strong&gt; &lt;code&gt;echo "$TARGET"&lt;/code&gt; immediately before you delete &lt;code&gt;"$TARGET"&lt;/code&gt;. Most &lt;code&gt;rm -rf&lt;/code&gt; disasters are a variable that expanded to nothing, or to the wrong path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refuse a blank expansion.&lt;/strong&gt; &lt;code&gt;set -u&lt;/code&gt; turns an unset variable into an error instead of a silent empty string, which is the difference between a complaint and a wiped directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check that the mount is mounted.&lt;/strong&gt; A &lt;code&gt;rm -rf&lt;/code&gt; on an unmounted &lt;code&gt;/mnt/backup&lt;/code&gt; cheerfully deletes the real directory sitting underneath it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove the backup, don't assume it.&lt;/strong&gt; Restore one file from last night before you ever need to restore all of them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://pragmaticsysadmin.help/sysadmin/2025-12-12-the-friday-backup-audit-because-hope-is-not-a-strategy/" rel="noopener noreferrer"&gt;Friday backup audit&lt;/a&gt; is a 20-minute version of that last line, and it beats rebuilding a server on a Saturday. When something does go sideways, &lt;a href="https://pragmaticsysadmin.help/sysadmin/2025-12-17-the-art-of-reading-logs-like-a-detective-finding-needles-in-haystacks/" rel="noopener noreferrer"&gt;reading the logs properly&lt;/a&gt; is where the fix comes from — not from panic.&lt;/p&gt;

&lt;p&gt;The transferable skill is the pause. Practising that pause in a simulator that scores you is cheap. Practising it on your only copy is how the stories start.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's the command you'd never run without &lt;code&gt;--dry-run&lt;/code&gt;? Tell me which one makes you double-check the path.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Related reads:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://pragmaticsysadmin.help/sysadmin/2025-12-12-the-friday-backup-audit-because-hope-is-not-a-strategy/" rel="noopener noreferrer"&gt;The Friday Backup Audit: Because Hope Is Not a Strategy&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://pragmaticsysadmin.help/sysadmin/2025-12-17-the-art-of-reading-logs-like-a-detective-finding-needles-in-haystacks/" rel="noopener noreferrer"&gt;The Art of Reading Logs Like a Detective: Finding Needles in Haystacks&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://pragmaticsysadmin.help/sysadmin/2025-12-09-the-5-minute-server-health-check-that-could-save-your-career/" rel="noopener noreferrer"&gt;The 5-Minute Server Health Check That Could Save Your Career&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sysadmin</category>
    </item>
  </channel>
</rss>
