<?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: Shubham Shrivastav</title>
    <description>The latest articles on DEV Community by Shubham Shrivastav (@shipguarde).</description>
    <link>https://dev.to/shipguarde</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%2F4022669%2F3feadbb1-ea21-47ff-b186-c22113b5847b.png</url>
      <title>DEV Community: Shubham Shrivastav</title>
      <link>https://dev.to/shipguarde</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shipguarde"/>
    <language>en</language>
    <item>
      <title>Pass, Fail, and the Third State Your QA Tool Is Missing</title>
      <dc:creator>Shubham Shrivastav</dc:creator>
      <pubDate>Wed, 19 Aug 2026 11:03:16 +0000</pubDate>
      <link>https://dev.to/shipguarde/pass-fail-and-the-third-state-your-qa-tool-is-missing-221c</link>
      <guid>https://dev.to/shipguarde/pass-fail-and-the-third-state-your-qa-tool-is-missing-221c</guid>
      <description>&lt;p&gt;Our release gate told us to ship a build it had never successfully looked at. Every one of the six checks had crashed. The report underneath the verdict said, in bold, &lt;strong&gt;Decision: BLOCK&lt;/strong&gt;. The field the API actually returns said &lt;code&gt;ship&lt;/code&gt;, and the one-line summary said "Cleared, all checks passed."&lt;/p&gt;

&lt;p&gt;Nothing was broken in the way you would expect. The model reasoned correctly. It wrote out its reasoning, and the reasoning was right. The failure happened in the two inches between the model's prose and the enum, and it failed in the direction that lets bad code through.&lt;/p&gt;

&lt;p&gt;We found that bug because we spent a day pointing our own agents at two dozen real marketing sites belonging to companies we wanted to talk to. The idea was to find something genuinely useful on each one and send it to them. What we got instead was a very fast education in the ways an automated QA tool can be confidently, articulately wrong, and all of them turned out to be the same mistake wearing different clothes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpsmprm8ly52f7vnfo7wh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpsmprm8ly52f7vnfo7wh.png" alt="Three states in, two states out: measured-good and never-measured both arrive at PASS as a single arrow, so a failed measurement is indistinguishable from a clean result." width="800" height="316"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Every false positive in this post is this diagram.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A green check on a build that never ran
&lt;/h2&gt;

&lt;p&gt;Start with the worst one, because it sets up everything else. Two of the sites we pointed the runner at were behind infrastructure that our browser could not get through. All six agents errored: &lt;code&gt;Audit returned 500&lt;/code&gt;, &lt;code&gt;Probe returned 500&lt;/code&gt;, &lt;code&gt;Page returned 0&lt;/code&gt;, &lt;code&gt;Could not capture rendered page snapshot&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The judge that produces the final call saw all of this. Its narrative said the release could not proceed. Its report rendered a table of six failed agents and the heading &lt;strong&gt;Decision: BLOCK&lt;/strong&gt;. Its reasoning field opened with the words "The decision is block based on hard guardrail (b)". There was no error anywhere in the pipeline: &lt;code&gt;judgeError&lt;/code&gt; was null.&lt;/p&gt;

&lt;p&gt;And the structured field that every integration is told to gate on read &lt;code&gt;ship&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The extraction step that turns the model's answer into an enum did not find the decision it was looking for, and fell back to a default. The default was the permissive value. In pull request mode that is a passing status check on a build nobody checked, sitting above a report that says, if you scroll, that it should have been blocked. Almost nobody scrolls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Absence is not evidence of absence
&lt;/h2&gt;

&lt;p&gt;The second shape is quieter and, for us, more dangerous, because it produces a finding rather than a non-finding.&lt;/p&gt;

&lt;p&gt;On one company's homepage our SEO agent reported that there was no canonical tag. We were about to send that to their engineering lead as a helpful observation. The page has a canonical tag. It is right there in the served HTML: &lt;code&gt;&amp;lt;link href="https://example.com/" rel="canonical"&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What happened is that the page capture had partially failed. Two other agents in the same run reported &lt;code&gt;Page returned 0&lt;/code&gt;. The SEO agent got a fragment of a document, looked for a canonical tag inside it, did not find one, and reported absence as a finding. The internal verifier then marked it confirmed, because it was checking that the agent had said the thing, not that the thing was true.&lt;/p&gt;

&lt;p&gt;This is the class of bug that ends a sales conversation before it starts. Telling a stranger their site is missing something that is visibly present demonstrates, in one message, exactly the failure mode you are asking them to pay you to prevent.&lt;/p&gt;

&lt;p&gt;Every absence-based check has this property. Missing alt text, missing lang attribute, missing meta description, missing heading, missing tag of any kind. All of them are only meaningful if you are certain you saw the whole document, and almost none of them check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blocked is not broken
&lt;/h2&gt;

&lt;p&gt;Our link checker reported a company's team page as containing a broken link, because a LinkedIn profile URL returned HTTP 999.&lt;/p&gt;

&lt;p&gt;999 is not a real status code. It is what LinkedIn returns to any client it does not believe is a browser. The control takes about four seconds to run: fetch the LinkedIn profile of someone unambiguously famous and still employed, and watch it also return 999. The profile is fine. The link is fine. The checker was blocked, and reported being blocked as evidence of breakage.&lt;/p&gt;

&lt;p&gt;We had already fixed this exact bug for HTTP 403 a day earlier, after a different site's trust page got flagged as broken when it was merely behind bot protection. The fix worked and the reasoning was sound. It just enumerated status codes instead of naming the concept, so the moment a different code with the same meaning appeared, the bug came back with a new number.&lt;/p&gt;

&lt;p&gt;The general rule turns out to be short: a status code that is not in the IANA registry cannot support a conclusion about whether a page exists. Neither can 403, 429, or anything else that means "I am refusing to talk to you specifically."&lt;/p&gt;

&lt;h2&gt;
  
  
  Whose failure was that, exactly?
&lt;/h2&gt;

&lt;p&gt;The fourth shape is the most embarrassing, because the tool blames its target for its own problems.&lt;/p&gt;

&lt;p&gt;On one site, our console and network agent reported twelve console errors and fourteen failed requests. Most of them were &lt;code&gt;net::ERR_CONNECTION_RESET&lt;/code&gt; against the site's CDN, and the specific files were the core framework bundles: the React bundle, the framework runtime, the animation library.&lt;/p&gt;

&lt;p&gt;Those files are fine. Fetched from an ordinary machine they return 200, at 170KB and 506KB respectively. The tell was available in the report itself and we nearly missed it: if a page's core React bundle had genuinely failed to load, the page would not have rendered, and the other agents would not have been able to describe its contents. The connection resets happened on our side. We reported them as fourteen defects in someone else's website, and our verifier confirmed every one.&lt;/p&gt;

&lt;p&gt;The same shape shows up whenever the measuring environment is unusual, which it always is. Headless browsers get served different content. Datacenter IPs get rate limited. Analytics scripts refuse to run for suspicious user agents, and then log a warning that looks a lot like a bug in the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The third state
&lt;/h2&gt;

&lt;p&gt;Four failures, one cause. In each case the system had two labels available, pass and fail, and the truth was neither. It had not measured. Forced to pick, it picked, and the choice was arbitrary in a way the output did not reveal.&lt;/p&gt;

&lt;p&gt;The fix is not smarter agents. It is a third outcome that sits alongside pass and fail and means something specific:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pass&lt;/strong&gt; means the check ran to completion and found nothing wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fail&lt;/strong&gt; means the check ran to completion and found something wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;undetermined&lt;/strong&gt; means the check did not run to completion, and the tool is declining to guess.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our link checker already had this concept, which is what makes the 999 bug so instructive. In the very same run that misreported the LinkedIn profile, it correctly emitted &lt;code&gt;was not checked because the origin rate-limited the check (429). This is not a broken link, it is a link we could not adjudicate.&lt;/code&gt; The vocabulary existed. One status code simply was not routed into it.&lt;/p&gt;

&lt;p&gt;That is the usual state of affairs. Teams that build this kind of tooling almost always discover the third state eventually, in one subsystem, and almost never propagate it to the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the default matters more than the detection
&lt;/h2&gt;

&lt;p&gt;Here is the part worth arguing about, because it is where we got it wrong in the most costly way.&lt;/p&gt;

&lt;p&gt;You will not catch every unmeasurable case. New ones appear constantly, because the web is adversarial to automated clients and getting more so. So the question is not only how you detect a failure to measure. It is what your code does when detection fails.&lt;/p&gt;

&lt;p&gt;In our verdict bug, the parser hit an input it could not classify and chose a default. That is reasonable. The problem is that the default was &lt;code&gt;ship&lt;/code&gt;. Somebody, at some point, wrote a fallback and reached for the value that would not annoy anyone, and that decision sat quietly in the codebase until the day it approved a build that had never been examined.&lt;/p&gt;

&lt;p&gt;The performance agent did the same thing in a different costume. On a page that returned 403 to our runner, and where three sibling agents correctly reported the 403, it produced &lt;code&gt;Performance 100/100&lt;/code&gt;. It had no measurements at all. Every metric was zero, and a composite built from zeros came out perfect. 100/100 is the most reassuring number the product can print, and it printed it for a page it had never seen.&lt;/p&gt;

&lt;p&gt;The rule we now hold to: &lt;strong&gt;when you cannot classify, degrade toward the answer that makes a human look.&lt;/strong&gt; Undetermined, not pass. An inconclusive build is annoying. A green build that was never tested is how a defect reaches production with documentation saying it was checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we fixed, and what is still wrong
&lt;/h2&gt;

&lt;p&gt;Being specific about this is the only reason a post like this is worth reading, so: our own defect backlog stands at 37 filed, most of them from pointing the tool at real sites over the past few weeks rather than at our own fixtures. 34 are fixed. Three are open as of today, all high severity, all customer-facing, and all three are the blocked-is-not-broken and whose-failure-is-it shapes described above.&lt;/p&gt;

&lt;p&gt;The verdict bug is fixed, and the fix has two halves, because one would not have been enough. The extraction is stricter, and the fallback now degrades to inconclusive rather than to ship. The summary line is derived from the agent results rather than from the decision, so "all checks passed" can no longer be printed over a table of six failures.&lt;/p&gt;

&lt;p&gt;The underlying capture bug is fixed too, and it was mundane in the way these things usually are. A screenshot call inside the navigation helper had a 30 second Playwright timeout. On a page that animates continuously, the screenshot never settles, the timeout raises, and because the screenshot lived inside &lt;code&gt;navigate()&lt;/code&gt; rather than beside it, the exception took out the navigation and therefore all six agents at once. One incidental screenshot, six checks, no results. The fix was to stop letting a picture fail a page load.&lt;/p&gt;

&lt;p&gt;What is still open is a fair reflection of how hard the general problem is. We can enumerate the bot-protection status codes we have seen. We cannot enumerate the ones we have not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist you can steal
&lt;/h2&gt;

&lt;p&gt;If you build, buy, or depend on anything that automatically checks a running application, these are worth asking. They took us a day of real use to learn and about an hour to write down.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can your tool say &lt;strong&gt;undetermined&lt;/strong&gt;? If the only outcomes are pass and fail, every unmeasurable case is already being silently mapped to one of them.&lt;/li&gt;
&lt;li&gt;When a check cannot classify its own result, what is the fallback value? Read the code, not the docs. If it is the permissive one, you have a fail-open gate.&lt;/li&gt;
&lt;li&gt;Do absence-based checks verify that the document fully loaded before concluding something is missing? A partial page makes everything look absent.&lt;/li&gt;
&lt;li&gt;Does the tool distinguish "this origin refused me" from "this resource does not exist"? Test it with a live URL behind bot protection and see what it says.&lt;/li&gt;
&lt;li&gt;Does it distinguish its own transport failures from the target's? A CDN that resets your connection is not a bug in the site you are measuring.&lt;/li&gt;
&lt;li&gt;Can a composite score be produced from zero measurements? Point it at a URL that returns 403 and see whether you get a number or an error.&lt;/li&gt;
&lt;li&gt;When your verifier confirms a finding, is it confirming the finding is &lt;em&gt;true&lt;/em&gt;, or only that the agent said it? These are very different checks, and the second one is much easier to build by accident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We build &lt;a href="https://shipguarde.com" rel="noopener noreferrer"&gt;ShipGuarde&lt;/a&gt;, which is where all of these came from. Writing this up was uncomfortable for the obvious reason, and worth it for a less obvious one: the day we found five ways our own tool could be wrong was the day it started being useful. A checker that knows the difference between a clean result and a failed measurement is a different product from one that does not, and you cannot tell them apart from the outside until something ships that should not have.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>Running Untrusted Code Safely: A Field Guide for AI and CI Pipelines</title>
      <dc:creator>Shubham Shrivastav</dc:creator>
      <pubDate>Fri, 10 Jul 2026 04:42:53 +0000</pubDate>
      <link>https://dev.to/shipguarde/running-untrusted-code-safely-a-field-guide-for-ai-and-ci-pipelines-48i2</link>
      <guid>https://dev.to/shipguarde/running-untrusted-code-safely-a-field-guide-for-ai-and-ci-pipelines-48i2</guid>
      <description>&lt;p&gt;&lt;em&gt;Every bot that reads your pull requests is executing code it did not write. Here is how to make sure that is not the end of the story.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There is a quiet assumption baked into most developer tooling: that the code we run is code we trust. Your CI runs your tests. Your linter loads your config. Your build installs your dependencies. For years that held, because the person triggering the job and the person who wrote the code were the same person.&lt;/p&gt;

&lt;p&gt;That assumption is now false, and a lot of teams have not noticed.&lt;/p&gt;

&lt;p&gt;The moment you add an AI reviewer, a CI job that runs on pull requests from forks, a dependency bot, or any automation that touches contributor code, you have invited untrusted code into a trusted environment. A stranger can now open a pull request and, if you are not careful, get your infrastructure to run something on their behalf. The unsettling part is not the code in the diff. It is everything that runs around it.&lt;/p&gt;

&lt;p&gt;This is a field guide to doing that safely. It is written for anyone building or operating a tool that reads other people's repositories, but the same principles apply to any CI pipeline that accepts pull requests from people you do not know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The threat is bigger than the diff
&lt;/h2&gt;

&lt;p&gt;When people picture untrusted code, they imagine a malicious function hidden in a pull request. That is real, but it is the least of your problems, because you are probably not executing the diff line by line. The real surface is everything your pipeline does automatically on the way to looking at that diff.&lt;/p&gt;

&lt;p&gt;Walk through what a normal review or CI job touches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Package installation.&lt;/strong&gt; Running &lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;pip install&lt;/code&gt;, or the equivalent executes lifecycle scripts. A single &lt;code&gt;postinstall&lt;/code&gt; hook, or one hostile transitive dependency, runs arbitrary code before a human has looked at anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The repository's own commands.&lt;/strong&gt; Test runners, build steps, and task files are code. If your pipeline runs &lt;code&gt;npm test&lt;/code&gt; on a contributor's branch, you are running whatever that contributor decided &lt;code&gt;test&lt;/code&gt; should mean today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool configuration.&lt;/strong&gt; This is the one almost everyone misses. Linters and formatters read configuration from the repository, and that configuration can load plugins, custom rules, and local modules. A crafted linter config is a remote code execution primitive wearing a yaml costume. The tool is yours. The config it obeys is theirs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the sharp version. The attacker does not need you to run their function. They need you to run your tool against their configuration. That is a far wider door, and it is usually left unlocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure that turns a bug into a breach
&lt;/h2&gt;

&lt;p&gt;A code execution bug inside an isolated sandbox is a contained incident. The same bug becomes a company-ending event when the environment running untrusted code also holds the keys to everything else.&lt;/p&gt;

&lt;p&gt;This is the anatomy of the worst tooling breaches. An automation platform runs contributor code in an environment that, for convenience, also contains its cloud credentials, its database connection string, and, in the most painful cases, the private key of the integration that grants it access to every customer repository. One escape from the sandbox, and the blast radius is not one job. It is every tenant at once.&lt;/p&gt;

&lt;p&gt;The lesson is not "sandbox harder." Sandboxes have escapes; assume yours will. The lesson is that the thing reading untrusted code should have nothing worth stealing and nowhere to pivot. If your isolation fails and the intruder finds an empty room with a dead network, you have had an incident worth a postmortem. If they find your master keys, you have had an extinction event. Design so that the worst realistic day is the boring one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven principles that actually hold
&lt;/h2&gt;

&lt;p&gt;Here is the posture I would defend in a design review. None of it is exotic. It is the discipline of assuming your isolation will eventually fail and making that failure uneventful.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never run the repository's own scripts.&lt;/strong&gt; No install, no test, no build, no task file from untrusted code. If you need to understand the project, parse its manifests yourself instead of executing its lifecycle. This single rule deletes the most common execution path outright.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep secrets out of the room.&lt;/strong&gt; The process that touches untrusted code should not have your platform credentials in its environment, its filesystem, or its memory. Not the database password, not the signing key, not another tenant's token. What is not there cannot leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope every credential to the minimum.&lt;/strong&gt; When the sandbox genuinely needs access, hand it the narrowest possible grant: a token scoped to the single repository, read-only, and short-lived. A token that can read one repo for ten minutes is a completely different liability from an app key that can write to a million.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deny egress by default.&lt;/strong&gt; Most analysis never needs the open internet, and it certainly does not need your internal services or the cloud metadata endpoint. Block outbound traffic and allow-list only what is required. This closes both data exfiltration and the classic metadata-credential theft in one move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat tool configuration as untrusted input.&lt;/strong&gt; If you run linters or analyzers, run them with plugin loading and config discovery disabled, or parse the repository-controlled config yourself rather than letting the tool obey it. Assume any file the attacker can write is an attempt to make your tools work for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the environment ephemeral, non-root, and constrained.&lt;/strong&gt; One run, one throwaway environment, destroyed when it finishes. Run as an unprivileged user, with a read-only filesystem except for a scratch directory, and hard limits on CPU, memory, and wall-clock time. Ephemerality alone defeats an entire category of persistence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practice least privilege on the integration itself.&lt;/strong&gt; Before you ever reach the sandbox, ask what your GitHub App or CI integration is even allowed to do. If it does not need write access to file contents, do not request it. The permissions you never hold cannot be abused when something downstream goes wrong.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;I care about this because I build a tool squarely in this category. ShipGuarde is an AI reviewer that reads pull requests and QAs the running app, which means it reads untrusted code every hour of every day. The posture above is not aspirational for us; it is the architecture. Each pull request is analyzed in a single-use sandbox that holds none of our platform's secrets. The only credential it ever receives is a short-lived token scoped to that one repository, read-only, and it has no route to internal services or metadata. We do not run a repository's own scripts, and we would rather parse a config than obey it. If that sandbox were fully compromised, the intruder would find one repository's readable source and a network cable that goes nowhere. That is the entire point.&lt;/p&gt;

&lt;p&gt;I mention it not to sell you anything, but because the constraint shaped every early decision we made, and I think far more tools should be built this way. If you are evaluating any automation that touches your source, this is the posture to make it prove.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist you can steal
&lt;/h2&gt;

&lt;p&gt;If you take nothing else, take this. Before you let any pipeline run on untrusted code, confirm every line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It does not execute the repository's install, test, or build.&lt;/li&gt;
&lt;li&gt;Its environment holds no platform or tenant secrets.&lt;/li&gt;
&lt;li&gt;Any credential it carries is repo-scoped, read-only, and short-lived.&lt;/li&gt;
&lt;li&gt;Outbound network access is denied by default and allow-listed narrowly.&lt;/li&gt;
&lt;li&gt;Repository tool configs cannot load plugins or arbitrary modules.&lt;/li&gt;
&lt;li&gt;The environment is ephemeral, non-root, and resource-limited.&lt;/li&gt;
&lt;li&gt;The integration requests the minimum permissions and nothing more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run down that list honestly. If you cannot check every box, you do not have a hardening problem. You have an untrusted-code problem in a hardening costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The industry spent a decade tuning pipelines for speed and convenience on the assumption that everything flowing through them was trusted. AI tooling quietly broke that assumption, and the tools built next will be judged on whether they noticed. The good news is that the safe design is not the slow one. When you stop running other people's code and refuse to hold secrets you do not need, you end up with a system that is both harder to breach and easier to reason about. For once, the secure path and the fast path are the same path. Take it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written from the trenches of building an AI reviewer. If your team is working through any of this, I am always happy to compare notes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>ai</category>
      <category>github</category>
    </item>
  </channel>
</rss>
