<?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: daniel gotlib</title>
    <description>The latest articles on DEV Community by daniel gotlib (@dgotlieb).</description>
    <link>https://dev.to/dgotlieb</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%2F4093253%2Fcfa38a7b-8b6c-46f2-b89c-018e09eaea74.jpg</url>
      <title>DEV Community: daniel gotlib</title>
      <link>https://dev.to/dgotlieb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dgotlieb"/>
    <language>en</language>
    <item>
      <title>I invented a CVE number to test my tool. It was real</title>
      <dc:creator>daniel gotlib</dc:creator>
      <pubDate>Thu, 27 Aug 2026 01:26:24 +0000</pubDate>
      <link>https://dev.to/dgotlieb/i-invented-a-cve-number-to-test-my-tool-it-was-real-3di1</link>
      <guid>https://dev.to/dgotlieb/i-invented-a-cve-number-to-test-my-tool-it-was-real-3di1</guid>
      <description>&lt;p&gt;I've been building a thing that checks whether the claims in a bug report&lt;br&gt;
correspond to anything that exists. Not whether the report is AI-generated —&lt;br&gt;
whether the file it names is in the repository, whether the function is&lt;br&gt;
declared, whether the version was ever tagged.&lt;/p&gt;

&lt;p&gt;To test it end to end I wrote a deliberately fabricated security report against&lt;br&gt;
one of my own projects. Fake file, fake function, fake commit hash, and a CVE&lt;br&gt;
number I made up on the spot: &lt;code&gt;CVE-2026-45871&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The tool came back and told me the CVE was real. It's a Linux kernel TPM bug —&lt;br&gt;
"st33zp24: Fix missing cleanup on get_burstcount() error." I had invented a&lt;br&gt;
number and hit a live one.&lt;/p&gt;

&lt;p&gt;Every other fabricated claim in that report was correctly flagged. The CVE, the&lt;br&gt;
one claim a triager would most want challenged, came back green and read like&lt;br&gt;
corroboration. My tool was asking "does this identifier exist" when the question&lt;br&gt;
that mattered was "does it have anything to do with this project."&lt;/p&gt;

&lt;p&gt;That's now the only open issue on the project, and it's a better bug than&lt;br&gt;
anything I found by staring at the code.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;Maintainers are getting buried in bug reports and security disclosures that read&lt;br&gt;
fluently and reference code that doesn't exist. The reports are cheap to&lt;br&gt;
generate and expensive to triage, and that asymmetry is the whole problem: a&lt;br&gt;
maintainer has to read carefully to find out there was nothing there.&lt;/p&gt;

&lt;p&gt;The useful property of these reports is that they hallucinate &lt;em&gt;plausible&lt;br&gt;
identifiers&lt;/em&gt;. &lt;code&gt;Curl_hpack_decode()&lt;/code&gt; instead of &lt;code&gt;Curl_hpack_decode_header()&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;lib/vtls/openssl_helper.c&lt;/code&gt;, which sounds exactly like a file curl would have.&lt;br&gt;
Identifiers are mechanically checkable. You don't need a model to ask whether a&lt;br&gt;
symbol is declared — you need a parser and a git repository.&lt;/p&gt;

&lt;p&gt;So: extract every checkable claim from the report, resolve each one against the&lt;br&gt;
repo at the revision the report says it affects, and report what didn't resolve.&lt;br&gt;
No judgement about who wrote it, no score, no auto-close.&lt;/p&gt;
&lt;h2&gt;
  
  
  The measurement
&lt;/h2&gt;

&lt;p&gt;Documentation is a convenient corpus and a bad one — it drifts, it's full of&lt;br&gt;
illustrative examples, it references other projects. Security advisories are the&lt;br&gt;
actual workload.&lt;/p&gt;

&lt;p&gt;curl publishes all 206 of its advisories in OSV format, each with the prose&lt;br&gt;
writeup and the exact affected version. Every one is human-written, every one was&lt;br&gt;
accepted as valid, and every claim in them was true of the release it describes.&lt;br&gt;
So any claim that fails to resolve is a false positive — with one controllable&lt;br&gt;
exception, which is checking an advisory against the wrong revision. The harness&lt;br&gt;
runs each advisory twice, once against HEAD and once against the release it&lt;br&gt;
actually names.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Corpus&lt;/th&gt;
&lt;th&gt;Claims&lt;/th&gt;
&lt;th&gt;Not found&lt;/th&gt;
&lt;th&gt;Unexplained&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;curl advisories, at the affected release&lt;/td&gt;
&lt;td&gt;129&lt;/td&gt;
&lt;td&gt;9.3%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.1%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;curl advisories, at HEAD&lt;/td&gt;
&lt;td&gt;124&lt;/td&gt;
&lt;td&gt;12.1%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;curl &lt;code&gt;docs/&lt;/code&gt; (4,449 files)&lt;/td&gt;
&lt;td&gt;1,165&lt;/td&gt;
&lt;td&gt;40.7%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;32.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Unexplained" means a miss carrying no hint. A miss that says &lt;em&gt;"no file by that&lt;br&gt;
name, but &lt;code&gt;lib/hpack.c&lt;/code&gt; exists"&lt;/em&gt; is useful to everyone. A bare miss on an honest&lt;br&gt;
report is what gets a tool uninstalled, so that's the column I care about.&lt;/p&gt;

&lt;p&gt;The gap between the two advisory rows is the cost of not telling it which&lt;br&gt;
release you mean. Check a 2019 advisory against today's HEAD and it will&lt;br&gt;
correctly tell you the function is gone, which is true and useless.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two bugs worth describing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tree-sitter's error recovery is arbitrary, and I was reading declarations off&lt;br&gt;
it.&lt;/strong&gt; curl declares every option through a macro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;CURLOPT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CURLOPT_AUTOREFERER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CURLOPTTYPE_LONG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;CURLOPT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CURLOPT_SSL_VERIFYPEER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CURLOPTTYPE_LONG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The C grammar has no rule for a macro-wrapped enumerator, so the parser&lt;br&gt;
error-recovers. Where the recovered ERROR node &lt;em&gt;ends&lt;/em&gt; is arbitrary: for one&lt;br&gt;
entry it stopped after &lt;code&gt;CURLOPT(&lt;/code&gt; and the name survived as a real enumerator,&lt;br&gt;
for the other it swallowed &lt;code&gt;CURLOPT(CURLOPT_AUTOREFERER&lt;/code&gt; whole and the name was&lt;br&gt;
never seen. Identical syntax, 22 lines apart in one enum, opposite answers.&lt;/p&gt;

&lt;p&gt;To a maintainer that doesn't read as "the claim was wrong." It reads as "this&lt;br&gt;
tool is broken." I now read enum bodies from their own text instead of trusting&lt;br&gt;
the recovered tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some names don't exist as text anywhere.&lt;/strong&gt; Through curl 7.62, every option was&lt;br&gt;
written like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#define CINIT(na, t, nu) CURLOPT_ ## na = t + nu
&lt;/span&gt;&lt;span class="n"&gt;CINIT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SSL_VERIFYPEER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LONG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&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;CURLOPT_SSL_VERIFYPEER&lt;/code&gt; appears &lt;strong&gt;zero times&lt;/strong&gt; in the header that declares it.&lt;br&gt;
The preprocessor builds it. And my candidate files were chosen by grepping for&lt;br&gt;
the name — so grepping found the docs that mention it and never the header that&lt;br&gt;
declares it. Fixing the parser did nothing until I fixed that too.&lt;/p&gt;

&lt;p&gt;Fixing both took the pinned advisory row from 24.0% unexplained to 3.1%. The&lt;br&gt;
four remaining misses are internal functions curl has since renamed or removed.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;It catches &lt;em&gt;lazy&lt;/em&gt; fabrication. A report that only names real symbols passes&lt;br&gt;
clean, and by design I have no way to distinguish a well-grounded fabrication&lt;br&gt;
from an honest report — the tool refuses to judge authorship, which is the whole&lt;br&gt;
reason a maintainer can run it without poisoning their contributor&lt;br&gt;
relationships.&lt;/p&gt;

&lt;p&gt;The 32% on curl's docs is the number I'd push back on if I were reading this.&lt;br&gt;
Documentation is a harsher corpus than reports — it's full of build variables,&lt;br&gt;
other projects' APIs, and illustrative examples — and some of those "misses" are&lt;br&gt;
correct: &lt;code&gt;CURLOPT_CONNECTIMEOUT&lt;/code&gt; is a typo in curl's own prose for&lt;br&gt;
&lt;code&gt;CURLOPT_CONNECTTIMEOUT&lt;/code&gt;, and reporting that it resolves to nothing is right. But&lt;br&gt;
I won't pretend that rate would be pleasant on a live issue tracker.&lt;/p&gt;

&lt;p&gt;Which is the honest limitation: every number above is a replay. No maintainer has&lt;br&gt;
run this against real inbound traffic, and "does it stay quiet enough on honest&lt;br&gt;
reports that you leave it enabled" is a question I can't answer by myself.&lt;/p&gt;

&lt;p&gt;If you maintain something that gets this kind of report, I'd genuinely like to&lt;br&gt;
know what it does on yours.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dgotlieb/substantiate@86b171f7b7afadbfd0cf95dd62a12b579f9078c5&lt;/span&gt;  &lt;span class="c1"&gt;# v0.1.4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;report&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.issue.body }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apache-2.0. Zero dependencies for the default path.&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>python</category>
    </item>
    <item>
      <title>Most Kubernetes clusters can't tell you what their GPUs cost</title>
      <dc:creator>daniel gotlib</dc:creator>
      <pubDate>Tue, 25 Aug 2026 23:05:21 +0000</pubDate>
      <link>https://dev.to/dgotlieb/most-kubernetes-clusters-cant-tell-you-what-their-gpus-cost-1ghl</link>
      <guid>https://dev.to/dgotlieb/most-kubernetes-clusters-cant-tell-you-what-their-gpus-cost-1ghl</guid>
      <description>&lt;p&gt;I built a small tool to answer one question: how many GPU-hours is a cluster paying for and not using?&lt;/p&gt;

&lt;p&gt;Then I pointed it at real production clusters. Not one of them could answer the question out of the box  and the reasons are the same everywhere, because they're all defaults nobody changes.&lt;/p&gt;

&lt;p&gt;Here's what I found, in the order it bit me.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The utilization metric everyone reports doesn't mean what you think
&lt;/h2&gt;

&lt;p&gt;Almost every GPU dashboard is built on &lt;code&gt;DCGM_FI_DEV_GPU_UTIL&lt;/code&gt;. It's the obvious choice  it's called utilization, it's a percentage, it goes up when things get busy.&lt;/p&gt;

&lt;p&gt;It reports &lt;strong&gt;the fraction of time at least one kernel was resident on the device.&lt;/strong&gt; Not whether that kernel did anything useful. A process that pins the GPU with a trivial loop reads 100% utilized while computing nothing at all.&lt;/p&gt;

&lt;p&gt;The metrics that tell you about real work are &lt;code&gt;DCGM_FI_PROF_SM_ACTIVE&lt;/code&gt; (streaming multiprocessor occupancy) and &lt;code&gt;DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code&gt; (tensor core activity). The gap between them is the interesting part: &lt;strong&gt;high engine activity with near-zero tensor activity means the GPU is busy doing something that isn't machine learning.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've started calling that ghost work. It's invisible on every dashboard I've seen, because those dashboards are plotting the wrong number.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Your GPU metrics are probably attributed to the wrong pod
&lt;/h2&gt;

&lt;p&gt;This one is worse, because it looks correct.&lt;/p&gt;

&lt;p&gt;dcgm-exporter only emits the &lt;em&gt;workload's&lt;/em&gt; pod labels when Kubernetes pod mapping is enabled  &lt;code&gt;DCGM_EXPORTER_KUBERNETES=true&lt;/code&gt;. Without it, Prometheus attaches the scrape target's own identity instead. Your GPU metrics come back tagged with &lt;code&gt;pod="nvidia-dcgm-exporter-xxxxx"&lt;/code&gt;, &lt;code&gt;namespace="monitoring"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the data looks complete. Every series has a namespace and a pod. Group by pod and you get a tidy chart. It's just that every GPU-hour in your cluster is attributed to the exporter that measured it.&lt;/p&gt;

&lt;p&gt;The tell is cheap to check: &lt;strong&gt;do the pods in your GPU metrics appear anywhere in &lt;code&gt;kube_pod_container_resource_requests&lt;/code&gt;?&lt;/strong&gt; If the intersection is empty, your attribution is fiction. One-line check, and I'd bet on the outcome more often than not.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The interesting counters are off by default
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;DCGM_FI_PROF_*&lt;/code&gt; metrics  the ones that distinguish real work from ghost work  require profiling to be enabled in the exporter's counter configuration. On the clusters I looked at, they weren't.&lt;/p&gt;

&lt;p&gt;That means the failure mode most worth catching is invisible on a default install, and you won't get an error telling you so. The metric simply returns nothing, and whatever you built on top quietly reports less than the truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The one that nearly fooled me
&lt;/h2&gt;

&lt;p&gt;This is the part I'd want to read, so it's the part I'll tell honestly.&lt;/p&gt;

&lt;p&gt;When profiling counters are missing, you can still estimate utilization from power draw. An idle A100 pulls around 55W against a 400W TDP; a busy one is near the ceiling. It's coarse, but idle versus working is unambiguous.&lt;/p&gt;

&lt;p&gt;I built that fallback with a generic 50–350W envelope for unknown GPU models. Then I ran it against a card that draws 15–130W.&lt;/p&gt;

&lt;p&gt;The tool reported a GPU running at 5% utilization with 88% of its framebuffer resident  textbook memory-parked, a model loaded and serving nothing. It put a confident &lt;strong&gt;$3,327/month&lt;/strong&gt; on it. I believed it. I told someone it was the first real evidence the whole idea worked.&lt;/p&gt;

&lt;p&gt;It wasn't real. The card was working fine. The generic envelope compressed its entire operating range into what looked like idle. When I fixed the fallback to prefer &lt;code&gt;GR_ENGINE_ACTIVE&lt;/code&gt;  a direct measurement rather than an inference from watts  the finding evaporated.&lt;/p&gt;

&lt;p&gt;The lesson isn't "watch your constants." It's that &lt;strong&gt;a cost tool that produces confident numbers from degraded inputs is worse than no tool at all.&lt;/strong&gt; The output was going to be screenshotted and taken to someone who approves budgets. A wrong number in that meeting doesn't just fail; it burns the credibility you needed to fix the real problem.&lt;/p&gt;

&lt;p&gt;Everything I built after that assumes the data is incomplete and says so out loud. It refuses to project a monthly cost from a window shorter than a day, because you haven't seen one diurnal cycle. It names every metric it couldn't find and which findings that suppresses. It labels utilization as estimated when it is. The report is less impressive and considerably more defensible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check on your own cluster
&lt;/h2&gt;

&lt;p&gt;Five minutes, read-only, no install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Are profiling counters enabled?&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:9090/api/v1/query &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'query=count({__name__=~"DCGM_FI_PROF_.*"})'&lt;/span&gt;

&lt;span class="c"&gt;# 2. Is pod mapping on? Compare these two sets.&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:9090/api/v1/query &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'query=count by (pod) (DCGM_FI_DEV_FB_USED)'&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:9090/api/v1/query &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'query=count by (pod) (kube_pod_container_resource_requests{resource="nvidia_com_gpu"})'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the first returns nothing, your profiling counters are off. If the pod names in the second and third queries don't overlap, your GPU metrics aren't attributed to the workloads consuming them.&lt;/p&gt;

&lt;p&gt;Both are one-line config changes. Neither is on by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gpuwaste&lt;/code&gt; is open source and does nothing clever: it ingests exported metrics, joins utilization against allocation, and reports how many GPU-hours you paid for and didn't use, with a dollar figure at the bottom.&lt;/p&gt;

&lt;p&gt;It's deliberately offline  it reads a CSV you export rather than connecting to your cluster, because production metric endpoints sit behind network policies and mTLS and most people never get past that. It also ships a synthetic data generator, so you can see exactly what it does in thirty seconds without touching anything real.&lt;/p&gt;

&lt;p&gt;Link in the comments. It's free and I'm not selling anything on the back of it.&lt;/p&gt;

&lt;p&gt;What I'd genuinely like: if you run it and the numbers look wrong for your setup, tell me how. Every cluster I've tested has broken it in a new way, and that's been the most useful part.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>gpu</category>
      <category>devops</category>
      <category>finops</category>
    </item>
    <item>
      <title>Your coding agent shouldn't run pytest</title>
      <dc:creator>daniel gotlib</dc:creator>
      <pubDate>Tue, 25 Aug 2026 03:14:13 +0000</pubDate>
      <link>https://dev.to/dgotlieb/your-coding-agent-shouldnt-run-pytest-4ak1</link>
      <guid>https://dev.to/dgotlieb/your-coding-agent-shouldnt-run-pytest-4ak1</guid>
      <description>&lt;p&gt;&lt;em&gt;First post in a build-in-public series about &lt;a href="https://github.com/Dgotlieb/verdict-mcp" rel="noopener noreferrer"&gt;verdict&lt;/a&gt;, an MCP server that gives coding agents structured, sandboxed test feedback.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Watch a coding agent work and you'll see it run &lt;code&gt;pytest&lt;/code&gt; in your shell, unsandboxed, and then push 40,000 tokens of raw output through its context window to answer one question: &lt;em&gt;did my change break anything?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's three problems in one command:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Token waste.&lt;/strong&gt; The agent needs ~10 lines of signal and pays for a wall of dots, warnings, and tracebacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No sandbox.&lt;/strong&gt; The tests run on your machine, in your environment, with your files writable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No memory.&lt;/strong&gt; When a test fails, the agent can't tell whether &lt;em&gt;it&lt;/em&gt; broke it or whether it was broken before it arrived - so it either "fixes" pre-existing failures nobody asked about, or ships regressions it assumes were already there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;verdict is an MCP server that replaces the pytest shell-out with four tools:&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;what it returns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verify(scope?)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;impact-selected tests, run in an ephemeral container, as a ~400-token typed verdict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;explain_failure(check_id)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the full traceback - only on demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;history(fingerprint)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;first seen / last seen / times seen for a failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;run_checks(["ruff","mypy"])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lint &amp;amp; type checks, same verdict shape&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;▶️ &lt;a href="https://github.com/Dgotlieb/verdict-mcp#readme" rel="noopener noreferrer"&gt;Watch the 30-second demo&lt;/a&gt; - Claude Code fixing a bug with verdict verifying in a container.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three ideas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Verdicts, not output.&lt;/strong&gt; &lt;code&gt;verify&lt;/code&gt; returns typed JSON: counts, per-failure message + location, and nothing else. Full tracebacks live behind &lt;code&gt;explain_failure&lt;/code&gt;. The whole verdict for a real failing run is ~400 tokens - the raw pytest output it replaces was ~40k. The design rule in the repo is blunt: &lt;em&gt;nothing bulky rides in the summary, ever.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Fingerprints give failures identity.&lt;/strong&gt; Every failure is hashed from its &lt;em&gt;normalized&lt;/em&gt; signature - volatile tokens (addresses, tmp paths, ids, durations) collapsed first. Same logical failure ⇒ same fingerprint, across runs and refactors. Fingerprints are what make the third idea possible:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. History answers "was it me?"&lt;/strong&gt; verdict keeps a small SQLite db per project. Every failure in a verdict carries &lt;code&gt;preexisting: true|false&lt;/code&gt; - &lt;em&gt;this exact failure was known before your change&lt;/em&gt; vs. &lt;em&gt;never seen it, it's yours&lt;/em&gt;. In the demo session that flag is the difference between an agent politely ignoring long-standing breakage and an agent burning a session "fixing" it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sandboxing posture (v0.1, honest version)
&lt;/h2&gt;

&lt;p&gt;Checks run in an ephemeral container (podman preferred, docker fallback, auto-detected - with liveness checks, because a podman binary with a stopped VM is worse than no podman at all). The worktree is mounted &lt;strong&gt;read-only&lt;/strong&gt; at &lt;code&gt;/src&lt;/code&gt;, copied to a writable &lt;code&gt;/work&lt;/code&gt; inside, and the check run gets &lt;code&gt;--network=none&lt;/code&gt;. Configured &lt;code&gt;setup_cmd&lt;/code&gt; runs &lt;em&gt;with&lt;/em&gt; network before the check; a prebuilt image is the tighter posture. No engine? An explicit &lt;code&gt;prefer = "local"&lt;/code&gt; fallback still runs against a temp copy - and if verdict ever runs somewhere other than where you configured, it says so in the verdict (&lt;code&gt;runner_note&lt;/code&gt;). No silent degradation is a design rule.&lt;/p&gt;

&lt;p&gt;Known gaps are written down in SECURITY.md rather than hand-waved: no resource limits yet, &lt;code&gt;setup_cmd&lt;/code&gt; is network-open by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  What dogfooding found (the embarrassing part)
&lt;/h2&gt;

&lt;p&gt;I wired verdict into Claude Code and asked it to fix a bug. Two things surfaced in the first hour that the 26-test suite had missed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Impact selection had never actually run.&lt;/strong&gt; The server ships as a console script, so the target project was never importable from its venv - grimp couldn't build the import graph, and selection silently(-ish) fell back to the full suite every time. My own test had papered over this with a &lt;code&gt;sys.path&lt;/code&gt; hack and an "honest fallback is acceptable" escape hatch. Deleted both, fixed for real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every scoped run executed the whole suite.&lt;/strong&gt; &lt;code&gt;pytest --json-report-omit&lt;/code&gt; takes &lt;code&gt;nargs='+'&lt;/code&gt;… and the test paths came right after it. pytest swallowed them as omit values. Ten characters of argument reordering.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both bugs made the headline feature a no-op while all tests were green. The suite now has the tests that would have caught them - written by breaking the real thing first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers from a real repo
&lt;/h2&gt;

&lt;p&gt;Cloned &lt;a href="https://github.com/theskumar/python-dotenv" rel="noopener noreferrer"&gt;python-dotenv&lt;/a&gt; (255 tests), dropped in a two-line &lt;code&gt;verdict.toml&lt;/code&gt;, no other changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;full suite in a container, deps installed on the fly: 255 tests, green, ~146s (dominated by &lt;code&gt;pip install&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;touch one hub module (&lt;code&gt;variables.py&lt;/code&gt;) → impact selection runs 184 tests, skips ~70, and says exactly how approximate the selection is in &lt;code&gt;selection_note&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That 146s is the next mountain: env setup needs caching (image reuse keyed on deps, result cache keyed on tree hash) before verify feels instant. That's v0.2, and it's written down as v0.2 - scope discipline is also a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uvx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"verdict-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"VERDICT_PROJECT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pip install verdict-mcp&lt;/code&gt; / &lt;code&gt;uvx verdict-mcp&lt;/code&gt;. Apache-2.0. Demo video and the full threat model in the &lt;a href="https://github.com/Dgotlieb/verdict-mcp" rel="noopener noreferrer"&gt;repo&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>mcp</category>
      <category>python</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
