<?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: Salman Ahsan</title>
    <description>The latest articles on DEV Community by Salman Ahsan (@salmanahsan).</description>
    <link>https://dev.to/salmanahsan</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%2F4071030%2Ff9ff3b31-d492-42d7-97e2-f7c82bb8a13e.jpg</url>
      <title>DEV Community: Salman Ahsan</title>
      <link>https://dev.to/salmanahsan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/salmanahsan"/>
    <language>en</language>
    <item>
      <title>I shipped 200 tools as single HTML files. Here's what that constraint actually buys you.</title>
      <dc:creator>Salman Ahsan</dc:creator>
      <pubDate>Thu, 27 Aug 2026 22:00:24 +0000</pubDate>
      <link>https://dev.to/salmanahsan/i-shipped-200-tools-as-single-html-files-heres-what-that-constraint-actually-buys-you-45pi</link>
      <guid>https://dev.to/salmanahsan/i-shipped-200-tools-as-single-html-files-heres-what-that-constraint-actually-buys-you-45pi</guid>
      <description>&lt;p&gt;Most of the tools I ship are one HTML file. Inline CSS, inline JavaScript, no build step, no bundler, no CDN, no npm install. You download the file, open it, it works. Offline, forever, on any browser.&lt;/p&gt;

&lt;p&gt;I've built a couple hundred this way now, and I want to lay out what that constraint actually buys and what it costs, because the internet's default answer to "should I use a framework" is unhelpfully binary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it buys&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Zero dependency rot. A file with no dependencies has nothing to break. No transitive vulnerability alerts, no deprecated package, no build that stops working because a toolchain moved on. A tool I wrote two years ago opens today and behaves identically. If you've maintained anything with a node_modules directory across a couple of years, you know what that's worth.&lt;/p&gt;

&lt;p&gt;Distribution is a file. No hosting, no deploy pipeline, no uptime. Email it, put it on a USB stick, commit it, drop it in a Slack channel. It runs from file://. For a paid tool this is the entire product: the customer owns the artifact and cannot lose access to it because of anything I do, including going out of business.&lt;/p&gt;

&lt;p&gt;Real privacy, not a privacy policy. No server means no server-side logs. Whatever the user types stays in their tab. For anything touching documents, keys, financial data, or client work, "this can't leave your machine because there's nowhere for it to go" is a categorically stronger claim than a promise, and it takes one sentence to explain.&lt;/p&gt;

&lt;p&gt;Speed you don't have to work for. One request, no hydration, no waterfall. There's nothing to optimize because there's nothing there.&lt;/p&gt;

&lt;p&gt;Auditability. View source and the whole program is in front of you. That's a genuine feature for security tooling. Anyone can read the entire thing in one sitting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it costs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd be lying if I framed this as free.&lt;/p&gt;

&lt;p&gt;No shared state. Every file is an island. A bug fixed in one is fixed in one. I've solved this with generation and templating rather than runtime sharing, which trades runtime coupling for build-time discipline, and that trade only works because I keep the tools small.&lt;/p&gt;

&lt;p&gt;No component reuse across files. Same header written in eighty files. When the header design changes, eighty files change. Again: tooling, not architecture.&lt;/p&gt;

&lt;p&gt;Real ceiling on complexity. This works beautifully up to a point and then it doesn't. Anything with auth, persistence across devices, collaboration, or genuinely complex state should not be a single file. I have a rough limit around 3,000 lines and once something wants to cross it, that's a signal the thing wants to be a real application.&lt;/p&gt;

&lt;p&gt;No ecosystem. No component library, no state management, no router. For most single-purpose tools you don't need any of it, and if you do need it, that's the same signal as above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The actual rule I use&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Single file when the problem fits in one screen of concept. A calculator, a converter, a checker, a generator, a formatter, an analyzer. One input, some processing, one output.&lt;/p&gt;

&lt;p&gt;The moment it needs a login or a database, it stops being a file and becomes an app, and I build it as one.&lt;/p&gt;

&lt;p&gt;That line is much further out than most developers assume. A surprising number of tools that ship as SaaS with an account and a subscription are, functionally, a form and a function. The account exists to enable the subscription, not because the tool needed one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this connects to AI tooling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The single-file approach turns out to pair well with BYOK. The user pastes their own API key, it lives in localStorage, the tool calls the provider directly from the browser. No proxy, no backend, no key ever touching my infrastructure.&lt;/p&gt;

&lt;p&gt;Which means an AI tool can also be one file. That still surprises people, and it's mostly a consequence of every provider offering a plain HTTPS endpoint and CORS being solvable.&lt;/p&gt;

&lt;p&gt;The constraint that started as an aesthetic preference turned out to be the thing that made a whole category of product possible at a price point that wouldn't otherwise work.&lt;/p&gt;

&lt;p&gt;There are a couple hundred of these at digitaldfy.com if you want to read the source of one. That's the point of shipping them this way: you can.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I build with AI and I am not coy about it. The facts, the decisions, and the judgment here are mine. The drafting is a tool, the same as every other tool I write about.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>html</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Your AI agent grades its own homework</title>
      <dc:creator>Salman Ahsan</dc:creator>
      <pubDate>Tue, 11 Aug 2026 05:29:07 +0000</pubDate>
      <link>https://dev.to/salmanahsan/your-ai-agent-grades-its-own-homework-2ekm</link>
      <guid>https://dev.to/salmanahsan/your-ai-agent-grades-its-own-homework-2ekm</guid>
      <description>&lt;p&gt;Here's a failure mode I keep running into, and I don't think it gets enough attention relative to how expensive it is.&lt;/p&gt;

&lt;p&gt;An AI coding agent finishes a task. It tells you what it did. "Ran the test suite, all passing. Updated the config. Cleaned up the temp files."&lt;/p&gt;

&lt;p&gt;You have essentially no way to check that, short of reading the entire session transcript line by line. And the transcript is thousands of lines, so nobody does.&lt;/p&gt;

&lt;p&gt;The agent is the only witness to its own behavior, and it's also the party with an interest in the summary sounding successful. That's not a claim about intent. It's just a structural problem: the model producing the summary is the same model that produced the work, working from the same context, with the same blind spots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the gap actually opens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claims that don't match execution. "All tests pass" when the test command errored out and the agent read the error as noise. "Migration applied" when it wrote the file but never ran it. These aren't hallucinations exactly, they're optimistic summaries of ambiguous outcomes.&lt;/p&gt;

&lt;p&gt;Secrets in the transcript. An agent runs cat .env once to check something. That key is now sitting in the session log, in plaintext, on disk, indefinitely. Nobody ever goes back and cleans it.&lt;/p&gt;

&lt;p&gt;Irreversible actions you didn't clock. In a long session, rm, force pushes, dropped tables, and sent requests scroll past. The summary at the end will not enumerate them. If you need to know exactly what can't be undone, the summary is the wrong artifact.&lt;/p&gt;

&lt;p&gt;Instructions quietly dropped. You wrote rules in your CLAUDE.md or your system prompt. Twenty thousand tokens later, adherence has drifted. The agent isn't lying about following them; it doesn't have a reliable view of its own compliance.&lt;/p&gt;

&lt;p&gt;Cost with no attribution. You know the session cost money. You don't know which step burned it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The approach: audit the transcript, not the summary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The transcript is the ground truth. It's a complete record of what actually happened, and unlike the summary it wasn't written to be reassuring. It's just too long for a human to read.&lt;/p&gt;

&lt;p&gt;So the fix is unglamorous: parse the transcript, look for specific things, report them. Deterministic checks against a file that already exists on your machine.&lt;/p&gt;

&lt;p&gt;I've been building small tools for this, each answering one question:&lt;/p&gt;

&lt;p&gt;Did the agent's claims match what actually executed?&lt;br&gt;
Did it leak secrets into the transcript?&lt;br&gt;
What irreversible actions did it take?&lt;br&gt;
Did it actually follow the rules you gave it?&lt;br&gt;
Where did the tokens and the money go?&lt;/p&gt;

&lt;p&gt;They're Python CLIs, zero dependencies, and they run against a finished session offline. No API calls, no keys, nothing uploaded. That last part matters: a tool built to find secrets in your transcript would be an absurd thing to ship if it had to send that transcript anywhere.&lt;/p&gt;

&lt;p&gt;They're open source at github.com/0xelitesystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why deterministic beats "ask another model"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The obvious alternative is to have a second model read the transcript and evaluate the first one. I don't think that works well for this, for two reasons.&lt;/p&gt;

&lt;p&gt;You've added a second thing that can be wrong, and you now need a way to check it. That regress doesn't terminate anywhere useful.&lt;/p&gt;

&lt;p&gt;And the questions worth asking are mostly not judgment calls. "Did a command that force-pushes appear in this session" is a string match, not an opinion. "Does this transcript contain something shaped like an API key" is a pattern match. Deterministic checks give you the same answer every time, run offline, cost nothing, and are auditable themselves. Use a model for the parts that genuinely need judgment, and don't use one for the parts that don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you take one thing from this&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Whatever tooling you use, adopt the habit: after any unattended agent session that touched anything real, check the transcript rather than the summary. Even manually. Even just grepping for destructive commands and for anything that looks like a credential.&lt;/p&gt;

&lt;p&gt;The general principle is one I keep coming back to across everything I build. Prefer the system that lets you verify it over the system that asks you to trust it. It's usually less impressive and it's almost always what you want at 2am when something has gone wrong.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
