<?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: Magithar Sridhar</title>
    <description>The latest articles on DEV Community by Magithar Sridhar (@magithar).</description>
    <link>https://dev.to/magithar</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%2F3863740%2F7aac0feb-68f1-4934-9ccf-be95a143de31.jpeg</url>
      <title>DEV Community: Magithar Sridhar</title>
      <link>https://dev.to/magithar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/magithar"/>
    <language>en</language>
    <item>
      <title>I Built a Nutrition-Scoring Bot Inside Telegram, and Swiggy Just Approved It</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Fri, 24 Jul 2026 00:58:55 +0000</pubDate>
      <link>https://dev.to/magithar/i-built-a-nutrition-scoring-bot-inside-telegram-and-swiggy-just-approved-it-2ko8</link>
      <guid>https://dev.to/magithar/i-built-a-nutrition-scoring-bot-inside-telegram-and-swiggy-just-approved-it-2ko8</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 1 of 2: the premise and the problem. Part 2 is the architecture deep-dive, how I cut the reply time from 30 seconds to 5.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I kept having
&lt;/h2&gt;

&lt;p&gt;I order a lot of packaged snacks off Swiggy Instamart. I almost never check the nutrition label before adding something to cart, not because I don't care, but because the label is a photo of tiny text three taps deep. By the time I find it, I've already decided I want the thing.&lt;/p&gt;

&lt;p&gt;I built it end to end (Python, FastAPI, Redis, the Telegram Bot API) as a side project, without a team. It's still an early, working prototype, not a polished product. A few weeks in, it got approved into &lt;strong&gt;Swiggy's Builders Club&lt;/strong&gt;, their official program for developers building on top of their platform (&lt;a href="https://www.linkedin.com/posts/magithar_buildinpublic-swiggy-productmanagement-share-7478012117481230337-cjhb/" rel="noopener noreferrer"&gt;I posted about the approval on LinkedIn&lt;/a&gt;). That was the point where "side project" started feeling like something worth writing up properly.&lt;/p&gt;

&lt;p&gt;The JTBD, in one line: &lt;strong&gt;see what you're eating before you buy it, without leaving your chat.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo Video:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/JJGoI5Rm2Yc"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Telegram, why solo
&lt;/h2&gt;

&lt;p&gt;No app to install, no account to create: if you can message a bot, you can use it. That constraint also forced the scope down to something a solo developer could actually ship: no UI to design beyond a chat card, no app store review, no separate onboarding flow.&lt;/p&gt;

&lt;p&gt;I built it end to end (Python, FastAPI, Redis, the Telegram Bot API) as a side project, without a team. It's still an early, working prototype, not a polished product. A few weeks in, it got approved into &lt;strong&gt;Swiggy's Builders Club&lt;/strong&gt;, their official program for developers building on top of their platform. That was the point where "side project" started feeling like something worth writing up properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that wasn't obvious going in
&lt;/h2&gt;

&lt;p&gt;The hard part of this project was never "call an API and show the result." Swiggy's product search doesn't return nutrition data at all, just name, brand, price, and a stock flag. Every score has to be &lt;em&gt;assembled&lt;/em&gt; from other sources: external food databases, web search, and, when nothing structured exists, an LLM taking its best guess.&lt;/p&gt;

&lt;p&gt;That's a very different engineering problem than a CRUD bot. It means deciding what to trust when your sources disagree or don't exist caching a &lt;em&gt;verified fact&lt;/em&gt; very differently from an &lt;em&gt;AI's best guess&lt;/em&gt; and, because "packaged snacks" here means Indian packaged snacks, realizing partway through that the standard European nutrition-scoring formula (Nutri-Score) doesn't actually discriminate well on the products people here are buying, and needs tuning&lt;/p&gt;

&lt;p&gt;The first working version of all this took &lt;strong&gt;~30 seconds&lt;/strong&gt; to reply. In a chat interface, that's dead on arrival.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Part 2 is the architecture post: how the reply time came down to 5-8 seconds, why the caching strategy encodes &lt;em&gt;confidence&lt;/em&gt; and not just staleness, and what it took to localize a food-scoring formula for a market it wasn't built for. If any of that sounds familiar (building on an API that doesn't give you the data you actually need, or caching LLM output you don't fully trust), that post is for you.&lt;/p&gt;

&lt;p&gt;Drop a comment if you want the Telegram link to try it, or if you've hit a similar "the API doesn't have the field I need" wall on something you're building. Curious what workarounds other people have landed on. (Code's closed-source for now while this is still prototype-stage; happy to answer questions about the approach in the comments.)&lt;/p&gt;




&lt;p&gt;&lt;em&gt;A working prototype, built solo with Python, FastAPI, Redis, and the Telegram Bot API. Currently part of Swiggy's Builders Club.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>showdev</category>
      <category>python</category>
      <category>telegram</category>
    </item>
    <item>
      <title>A Reader Asked What My Security Check Actually Verifies. The Honest Answer Sent Me Back to the Code.</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Fri, 17 Jul 2026 17:09:02 +0000</pubDate>
      <link>https://dev.to/magithar/a-reader-asked-what-my-security-check-actually-verifies-the-honest-answer-sent-me-back-to-the-1fif</link>
      <guid>https://dev.to/magithar/a-reader-asked-what-my-security-check-actually-verifies-the-honest-answer-sent-me-back-to-the-1fif</guid>
      <description>&lt;p&gt;Someone replied on Mastodon to my &lt;a href="https://dev.to/magithar/a-high-score-means-nothing-if-the-tool-is-dangerous-so-i-added-a-security-gate-3hpc"&gt;launch post&lt;/a&gt; with a question I didn't have a good answer for:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;curious what the security check actually verifies. known CVEs / package provenance is one bar, "this maintainer won't quietly change hands in 8 months" is a much harder one and honestly the one that's bitten people more. still, a tool suggesting the right library beats finding out mid-project it was the wrong one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I went to check what "security check" actually meant in my own skill, expecting to defend it. Instead I found the weakest rule in the whole file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Actually There
&lt;/h2&gt;

&lt;p&gt;Phase 3.5's hard gate had this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DISCARD (set security: "BLOCKED") if:
- Has a dependency with a known CVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No data source. No API. Nothing telling the model where to look. In practice this depended entirely on whatever a web search happened to surface for that candidate, which is a fancy way of saying it usually did nothing. And the commenter's actual point, maintainer handoff, wasn't checked at all. Contributors were extracted in Tier 1 and then never used anywhere.&lt;/p&gt;

&lt;p&gt;Two gaps, and the second one was the sharper of the two. A known CVE gets patched, gets a CVSS score, gets a GitHub Security Advisory. A quiet maintainer handoff has none of that until someone notices the malware. In event-stream's case, below, that took months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Real CVE Check
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://osv.dev" rel="noopener noreferrer"&gt;OSV.dev&lt;/a&gt; is free, unauthenticated, and covers npm, PyPI, Go, and crates.io from one endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.osv.dev/v1/query"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"package":{"name":"lodash","ecosystem":"npm"},"version":"4.17.15"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Straightforward, except for one trap I only found by testing against a real vulnerable package. Query &lt;code&gt;requests==2.19.0&lt;/code&gt; on PyPI and you get advisories back labeled &lt;code&gt;PYSEC-2018-28&lt;/code&gt; with &lt;code&gt;severity: UNKNOWN&lt;/code&gt;. Looks like nothing to worry about. But &lt;code&gt;aliases&lt;/code&gt; links it to &lt;code&gt;GHSA-x84v-xcm2-53pg&lt;/code&gt;, and that one is rated HIGH. PyPI's advisory feed duplicates every GHSA entry under its own ID, and the PyPI-native copy just doesn't carry a severity field. Skip the dedupe and the one advisory that matters hides behind a label that says "unknown."&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Publisher-Continuity Check, Badly, Three Times
&lt;/h2&gt;

&lt;p&gt;This is the one the commenter actually asked for, and getting it right took three attempts, each one falsified by testing against real npm data instead of my assumptions about what would work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 1: flag any human-to-human publisher change.&lt;/strong&gt; Tested it against &lt;code&gt;express&lt;/code&gt;. False positive. It's not a handoff, it's a healthy team rotating releases among four maintainers over a decade. Same failure on &lt;code&gt;lodash&lt;/code&gt; and &lt;code&gt;chalk&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2: require that the old publisher never publishes again.&lt;/strong&gt; This is the technically correct definition of a handoff, and it's useless. Tested against 13 well-known packages: it fired on 8 of them, including &lt;code&gt;react&lt;/code&gt;, &lt;code&gt;debug&lt;/code&gt;, and &lt;code&gt;semver&lt;/code&gt;. Nearly every long-lived package has &lt;em&gt;some&lt;/em&gt; handoff somewhere in its history. A rule that fires on most of npm isn't a signal, it's noise, and noise trains people to stop reading warnings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 3: same rule, but only report the most recent handoff, and only if it's under 12 months old.&lt;/strong&gt; This is what shipped. Swept it across 98 popular packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;n=98  FIRE=7 (7.1%)  STALE=50 (51.0%)  CLEAN=41
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;51% of those packages carry a stale handoff somewhere in their past. The recency filter is doing the real work. Without it, this thing is worthless.&lt;/p&gt;

&lt;p&gt;I broke it once more trying to "improve" it. I replaced the hardcoded bot exclusion list (&lt;code&gt;GitHub Actions&lt;/code&gt;, &lt;code&gt;npm&lt;/code&gt;, etc.) with a regex, thinking it would generalize better to automation accounts I hadn't thought of. Fire rate on the same 98 packages jumped from 7.1% to 33.7% overnight. The bug: &lt;code&gt;GitHub Actions&lt;/code&gt; has a space in it, my character class only covered &lt;code&gt;[-_]&lt;/code&gt;, so the single most important filter silently stopped matching and every CI-migrated package started reading as a handoff.&lt;/p&gt;

&lt;p&gt;Fixed the regex, then measured it head-to-head against the plain hardcoded list on the same list again. Identical result, 7.1% both. The regex bought nothing. Its one apparent win was a rule I'd written specifically to catch a package I'd just watched it fail on, which is fitting my sample, not generalizing. I threw it out and kept the simpler thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does the Recency Filter Still Catch event-stream?
&lt;/h2&gt;

&lt;p&gt;The real test is &lt;code&gt;event-stream&lt;/code&gt;, the 2018 incident where &lt;code&gt;right9ctrl&lt;/code&gt; took over publish rights from the original maintainer and shipped a targeted credential-stealing payload three versions later. I ran the check as if I were adopting the package at different points in time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;as of 2018-08-01 (before handoff)             -&amp;gt; no handoff
as of 2018-10-01 (before malware disclosed)   -&amp;gt; WARN: dominictarr -&amp;gt; right9ctrl
as of 2018-11-26 (disclosure day)             -&amp;gt; WARN: dominictarr -&amp;gt; right9ctrl
as of 2026-07-17 (today)                      -&amp;gt; 94mo old, not reported
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handoff happened on 2018-09-05. My 12-month recency window means the check would have flagged it any time up to 2019-09-05, and specifically on 2018-10-01, a full seven weeks before the malicious code was publicly disclosed on 2018-11-26. On the day it mattered, OSV would have returned nothing. Advisories don't exist before disclosure. That's the exact complementarity the commenter was pointing at: one check catches what's already known, the other catches something the industry hasn't found yet.&lt;/p&gt;

&lt;p&gt;Real fires from the 98-package sweep, none of them noise: &lt;code&gt;dougwilson&lt;/code&gt; stepping back from &lt;code&gt;cors&lt;/code&gt; and &lt;code&gt;morgan&lt;/code&gt;, and &lt;code&gt;wikirik&lt;/code&gt; picking up both &lt;code&gt;sequelize&lt;/code&gt; and &lt;code&gt;validator&lt;/code&gt; in the same window. That second one isn't evidence of anything wrong, but it's exactly the kind of pattern worth a human glance instead of a silent PASS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Doesn't Catch
&lt;/h2&gt;

&lt;p&gt;Worth saying plainly, because a security gate that oversells its coverage is worse than no gate. &lt;code&gt;ua-parser-js&lt;/code&gt;, &lt;code&gt;rc&lt;/code&gt;, and &lt;code&gt;coa&lt;/code&gt; are three real npm supply-chain compromises, and in all three the attacker published under the legitimate maintainer's own account. Publisher continuity reads completely clean on all of them. Only the OSV check catches those, and only after someone disclosed it.&lt;/p&gt;

&lt;p&gt;And publisher continuity is npm-only. PyPI's package index has no per-release uploader field at all, just an upload timestamp. Python candidates get an explicit &lt;code&gt;N/A (unsupported ecosystem)&lt;/code&gt; instead of a number that implies something was checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Found One For Real
&lt;/h2&gt;

&lt;p&gt;I ship checks, then I re-run whatever eval I already had to make sure I didn't just add a rule that looks right and does nothing. I have a paired skill-on/skill-off comparison that keeps recommending vector databases for the same test project, so I re-ran that same prompt to see if the new OSV check actually changed anything.&lt;/p&gt;

&lt;p&gt;It did. &lt;code&gt;chromadb&lt;/code&gt;, the library that had won every previous run of that comparison, came back with &lt;code&gt;CVE-2026-45829&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.osv.dev/v1/query"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"package":{"name":"chromadb","ecosystem":"PyPI"},"version":"1.5.9"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CRITICAL, pre-authentication code injection, and no patched version at any point in the current release history. I didn't take OSV's word for the "no fix" part, I checked the GitHub Advisory API directly too: &lt;code&gt;first_patched_version: null&lt;/code&gt;. Chroma got discarded before it ever reached scoring, for the first time in every run I'd done. The rule wasn't decoration, it changed a real recommendation the moment it had a real vulnerability to work with.&lt;/p&gt;

&lt;p&gt;Reading the actual advisory, it only fires in Chroma's server mode with &lt;code&gt;trust_remote_code&lt;/code&gt; set to true, a flag most embedded, in-process usage never sets. I still discarded it unconditionally, that's the point of a hard CRITICAL-with-no-fix rule, but a block that doesn't say why isn't much more useful than no explanation at all. So the DISCARD rule now also carries the advisory's stated trigger condition into &lt;code&gt;security_note&lt;/code&gt;, quoted straight from OSV instead of summarized by me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DISCARD (set security: "BLOCKED", add security_note) if:
- OSV returns a CRITICAL or HIGH advisory ... with no `fixed` version available
  — set security_note to the advisory's summary, including any trigger
  condition (specific mode, flag, or endpoint) verbatim from OSV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still blocks regardless of the condition. The note doesn't narrow the rule, it just means the person reading the block can tell "unsafe for me" apart from "unsafe in a mode I don't use," instead of taking a bare BLOCKED at face value.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;Live in v1.4.6, across all four adapters. Phase 3.5 now runs both checks against live data before scoring anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add Magithar/SKILLmama &lt;span class="nt"&gt;-a&lt;/span&gt; claude-code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;&lt;/strong&gt;, Apache 2.0.&lt;/p&gt;

&lt;p&gt;The lesson from this one isn't really about supply-chain security. It's that "make it more general" is not automatically an improvement, and the only way I found that out was by measuring the before and after on the same data instead of trusting that the more sophisticated version was the better one. Twice in this round the simpler rule beat the smarter-looking one, and both times, only running it and counting told me which. And the check itself only proved it was more than decoration once I went back and re-ran the same eval that had been sitting there the whole time, which is exactly what caught Chroma.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>openai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Asked the Same Question With and Without My Own Skill. The Version Without It Won on One Thing That Mattered.</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Fri, 17 Jul 2026 16:56:31 +0000</pubDate>
      <link>https://dev.to/magithar/i-asked-the-same-question-with-and-without-my-own-skill-the-version-without-it-won-on-one-thing-5gjn</link>
      <guid>https://dev.to/magithar/i-asked-the-same-question-with-and-without-my-own-skill-the-version-without-it-won-on-one-thing-5gjn</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/magithar/i-watched-a-talk-called-dont-ship-skills-without-evals-then-checked-if-mine-had-any-1k0e"&gt;last post&lt;/a&gt;, I ran an eval harness against SKILLmama and found two bugs. There was a third finding I held back, because it came from a different kind of test, and it's the one that actually changed a real recommendation.&lt;/p&gt;

&lt;p&gt;The idea is simple, and it comes straight from the paired-evaluation method in the SkillsBench paper: take one question, run it twice, once with the skill, once without, same project, same everything else. Compare what comes back.&lt;/p&gt;

&lt;p&gt;I hadn't actually done this yet. Every eval run up to that point only tested SKILLmama with itself turned on. So I ran the real comparison, on a real external project, not a toy example.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;Project: &lt;code&gt;nutri-bot&lt;/code&gt;, a Telegram bot I built separately, FastAPI backend, Redis for caching, Gemini and Groq for LLM calls, deployed on Render's free tier. No vector database anywhere in it yet.&lt;/p&gt;

&lt;p&gt;Question, asked both ways: "What vector DB should I use for RAG in my FastAPI app?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skill-off:&lt;/strong&gt; a plain agent, given read access to the repo, no web search, no structured pipeline, just its own judgment plus whatever it found by reading the files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skill-on:&lt;/strong&gt; SKILLmama, full pipeline, live web search, verified GitHub stars and PyPI downloads, security screening, the whole thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Skill-Off Said
&lt;/h2&gt;

&lt;p&gt;It read &lt;code&gt;requirements.txt&lt;/code&gt;, &lt;code&gt;.env.example&lt;/code&gt;, and &lt;code&gt;SETUP.md&lt;/code&gt;, then recommended pgvector or Upstash Vector, and specifically warned against Chroma or FAISS running in-process:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On Render's free/small web dynos your process can restart or spin down, so an in-memory or local-disk vector index disappears or desyncs. Only reasonable if your corpus is tiny and rebuildable in a few seconds on startup.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It also tied the embedding model choice to the LLM already in use (Gemini's &lt;code&gt;text-embedding-004&lt;/code&gt;, since the app already calls Gemini) and asked what the RAG was actually for before locking in a recommendation.&lt;/p&gt;

&lt;p&gt;No live verification. It said so itself when I asked it to self-report: five options seriously compared, no star counts or download numbers checked, pure judgment from reading the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Skill-On Said
&lt;/h2&gt;

&lt;p&gt;SKILLmama's pipeline ran the full search, verified real numbers, and produced a scored table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;Compat&lt;/th&gt;
&lt;th&gt;Pop&lt;/th&gt;
&lt;th&gt;Maint&lt;/th&gt;
&lt;th&gt;Simple&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chroma&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qdrant&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;7.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pgvector&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5.55&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Chroma came out on top, recommended specifically because it's in-process and needs no extra infrastructure.&lt;/p&gt;

&lt;p&gt;That's the exact property skill-off had just flagged as a deployment risk on this platform. SKILLmama's Compatibility check verifies things like whether a required env var exists or whether a CLI is on PATH. It had no check for whether the app's actual hosting target keeps local disk around between restarts. So it confidently scored the one candidate that would silently lose its data on the next deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Uncomfortable Part
&lt;/h2&gt;

&lt;p&gt;Grounded, verified, live-searched data still lost to a plain agent that just read &lt;code&gt;SETUP.md&lt;/code&gt; and thought about it for a second. Not because the plain agent was smarter. Because it happened to look at the one file that mattered for this specific question, and my scoring formula had never told SKILLmama to look there at all.&lt;/p&gt;

&lt;p&gt;This is exactly the failure mode Schmid's talk was warning about, and exactly why "run the ablation" isn't optional. If I'd stopped after the first eval run, where I only tested SKILLmama against itself, I would have shipped a skill that confidently recommends the wrong answer for a common deployment shape, and never known.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Phase 4 now runs a Deployment Persistence Check before scoring any candidate that stores data locally or in-process. It looks for the same signals the plain agent found by reading a doc file, but does it systematically:&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="s"&gt;fly.toml      -&amp;gt; persistent only if it has a [[mounts]] section&lt;/span&gt;
&lt;span class="s"&gt;railway.toml  -&amp;gt; persistent only if a volume is configured&lt;/span&gt;
&lt;span class="na"&gt;render.yaml   -&amp;gt; persistent only if it has a disk&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;block&lt;/span&gt;
&lt;span class="s"&gt;vercel.json   -&amp;gt; always ephemeral, serverless by design&lt;/span&gt;
&lt;span class="na"&gt;docker-compose.yml -&amp;gt; persistent only if volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;maps the data path&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a deployment target is detected and none of that is configured, Compatibility gets capped and the output carries an explicit warning instead of a clean-looking number.&lt;/p&gt;

&lt;p&gt;Re-ran the same question after the fix. Chroma's Compatibility dropped from 8 to 5, with the warning attached. The score gap to Qdrant narrowed to under half a point, which triggered SKILLmama's own tiebreaker rule, and the output now recommends Qdrant Cloud as the safer default if the data is going to accumulate over time, Chroma only if the corpus is small enough to rebuild on every cold start. Same conclusion the plain agent reached, just with verified stars, downloads, and a security pass attached to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the Same Prompt Found Something Worse
&lt;/h2&gt;

&lt;p&gt;That wasn't the end of it. The old security gate had a rule that just said "has a dependency with a known CVE," with no data source behind it, so in practice it depended on whatever a search happened to surface that day. I replaced it with a real check: query &lt;a href="https://osv.dev" rel="noopener noreferrer"&gt;OSV.dev&lt;/a&gt; for the exact version being recommended, across npm, PyPI, Go, and crates.io.&lt;/p&gt;

&lt;p&gt;Then I re-ran this same eval to make sure the new check actually worked end to end. It did, and it took out the library that had won every single prior run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.osv.dev/v1/query &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"package":{"name":"chromadb","ecosystem":"PyPI"},"version":"1.5.9"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chroma's current latest release carries &lt;code&gt;CVE-2026-45829&lt;/code&gt;, a CRITICAL pre-authentication code injection vulnerability, and there is no patched version. I checked that directly against the GitHub Advisory API rather than trusting the OSV response alone: &lt;code&gt;first_patched_version: null&lt;/code&gt;. Chroma is now BLOCKED before it reaches scoring, and Qdrant wins outright, no narrow tiebreaker needed.&lt;/p&gt;

&lt;p&gt;Reading the advisory closer, it only triggers in Chroma's server mode with &lt;code&gt;trust_remote_code&lt;/code&gt; set to true, an opt-in flag most embedded, in-process usage never touches, including nutri-bot's. The rule still blocks regardless of that, on purpose. I don't want a discovery skill quietly deciding a CRITICAL CVE doesn't apply to you. But a block with no explanation isn't much better than no block at all, so the same day, I changed the rule to quote the advisory's stated trigger condition straight from OSV into the output. Still an unconditional block, now with the context to judge it yourself instead of taking my word for it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;Live in v1.4.6, across all four adapters. The full run, both outputs unedited, is in &lt;a href="https://github.com/Magithar/SKILLmama/blob/main/evals/skill-on-vs-skill-off-comparison.md" rel="noopener noreferrer"&gt;&lt;code&gt;evals/skill-on-vs-skill-off-comparison.md&lt;/code&gt;&lt;/a&gt;, and the run-by-run log including this one is in &lt;a href="https://github.com/Magithar/SKILLmama/blob/main/evals/skillmama-ablation.md" rel="noopener noreferrer"&gt;&lt;code&gt;evals/skillmama-ablation.md&lt;/code&gt;&lt;/a&gt;, if you want the actual transcripts instead of my summary of them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add Magithar/SKILLmama &lt;span class="nt"&gt;-a&lt;/span&gt; claude-code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;&lt;/strong&gt;, Apache 2.0.&lt;/p&gt;

&lt;p&gt;The lesson isn't "plain agents beat structured pipelines." It's that a pipeline is only as good as the checks someone remembered to put in it, and the fastest way to find the one you forgot is to run the version without it side by side and see where it wins. The second lesson, from finding the CVE, is that "the checks someone remembered to put in" needs re-running every time you change what those checks actually do, not just once when you write them.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Watched a Talk Called "Don't Ship Skills Without Evals," Then Checked If Mine Had Any</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Fri, 17 Jul 2026 16:52:19 +0000</pubDate>
      <link>https://dev.to/magithar/i-watched-a-talk-called-dont-ship-skills-without-evals-then-checked-if-mine-had-any-1k0e</link>
      <guid>https://dev.to/magithar/i-watched-a-talk-called-dont-ship-skills-without-evals-then-checked-if-mine-had-any-1k0e</guid>
      <description>&lt;p&gt;I came across a talk by Philipp Schmid from Google DeepMind, titled "Don't Ship Skills Without Evals." His point, boiled down: everyone's writing skills for AI agents right now, almost nobody tests them, and because LLMs are non-deterministic, eyeballing the output during development ("looks right to me") tells you almost nothing about what happens once the thing hits real usage.&lt;/p&gt;

&lt;p&gt;That landed uncomfortably close to home. SKILLmama has four skill files, install instructions across four agents, and a scoring formula I'd been trusting on faith since v1.0. I'd never once actually tested whether it did what it claimed. So I built a small eval harness and ran it. Three bugs, first try.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Actually Built
&lt;/h2&gt;

&lt;p&gt;Nothing fancy. A markdown file at &lt;code&gt;evals/skillmama-ablation.md&lt;/code&gt;: five prompts that should trigger SKILLmama, five that shouldn't, mapped straight to the Trigger and Do-NOT-activate rules already in the skill file. A result log underneath it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Should trigger:
1. "What vector DB should I use for RAG in my FastAPI app?"
2. "Find me the best job queue for my Node stack, self-hosted only."
3. "/skillmama" (no args)
4. "Scan my project and tell me what I'm missing."
5. "Recommend something for authentication, must have MCP support."

Should NOT trigger:
6. "How do I configure Redis pub/sub in Python?"
7. "Why is my pgvector query returning zero results?"
8. "Can you review this auth middleware I wrote for security issues?"
9. "I've decided to use Stripe for payments, walk me through the webhook setup."
10. "What's the difference between REST and GraphQL?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First run: 10 for 10. The trigger logic held up. Good sign, but it's the easy part of the test. It only checks whether the skill turns on. The harder question is whether the pipeline is any good once it fires, and answering that meant pointing it at an actual project instead of a checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug One: Silent Failure on the Wrong Directory
&lt;/h2&gt;

&lt;p&gt;I ran the vector-DB prompt from inside SKILLmama's own repo, mostly because that's where my terminal happened to be. SKILLmama scanned the current directory for FastAPI files, correctly found none (it's a docs repo, not a FastAPI app), and just... proceeded. Empty stack profile, and it kept searching anyway like nothing was wrong.&lt;/p&gt;

&lt;p&gt;That's the kind of failure nobody reports, because nothing looks broken. It just quietly gives worse answers.&lt;/p&gt;

&lt;p&gt;The fix: if the user states a stack inline and the scanned directory shows no trace of it, stop and ask which directory to scan instead of completing an empty pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I didn't find FastAPI in this directory, this doesn't look like that
project. Want me to scan a different folder, or should I search based
on what you told me with no local verification?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bug Two: Confident Numbers That Weren't Verified
&lt;/h2&gt;

&lt;p&gt;SKILLmama's Compatibility score already had a rule: verify locally before scoring, don't just infer. Check &lt;code&gt;.env.example&lt;/code&gt; for the right env var, check whether a required CLI is on PATH, don't guess.&lt;/p&gt;

&lt;p&gt;Maintenance, sitting right next to it in the same formula, had no equivalent rule. When I ran an actual search, two of three candidates got their last-commit date from a search result. The third got a number from general knowledge of how active that org usually is. All three sat in the same table, looking equally solid.&lt;/p&gt;

&lt;p&gt;Fix: Maintenance now runs by the same rule Compatibility already had, verify before scoring, don't infer. If a real last-commit date can't be found, the score shows &lt;code&gt;N/A (unverified)&lt;/code&gt; instead of a confident-looking guess, and the total renormalizes across whatever factors are left.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Three Needed a Different Kind of Test
&lt;/h2&gt;

&lt;p&gt;The first two bugs came from just running the pipeline and watching it. The third one needed something closer to what Schmid's talk was actually about: comparing the skill against no skill at all, same question, same project, run in parallel. That's its own story, long enough to deserve a separate post rather than a paragraph here. Short version: it exposed a blind spot in how SKILLmama scored a candidate's fit for a specific hosting platform, and fixing it flipped an actual recommendation. That fix ships alongside that post, not with the two below.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;The first two fixes are live now, v1.4.4 and v1.4.5, across all four adapters (Claude Code, Claude.ai, OpenAI Codex, and Antigravity), kept in sync so none of them quietly drift from the others.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add Magithar/SKILLmama &lt;span class="nt"&gt;-a&lt;/span&gt; claude-code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to see the full run log, exact prompts, exact outputs, both bugs as they actually surfaced and not cleaned up after the fact, the eval harness lives at &lt;a href="https://github.com/Magithar/SKILLmama/blob/main/evals/skillmama-ablation.md" rel="noopener noreferrer"&gt;&lt;code&gt;evals/skillmama-ablation.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;&lt;/strong&gt;, Apache 2.0.&lt;/p&gt;

&lt;p&gt;Credit where it's due: this whole detour started with Philipp Schmid's talk and the SkillsBench paper it references (Li et al., &lt;a href="https://arxiv.org/abs/2602.12670" rel="noopener noreferrer"&gt;arxiv.org/abs/2602.12670&lt;/a&gt;). It found that human-written skills lift task performance by around 15 to 16 percentage points on average, and that AI-generated skills often add filler that does nothing but burn tokens. If you maintain a skill and haven't run it against a labeled prompt set yet, that's the whole homework assignment. Took less time than writing this post did.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>openai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Bug Was in My Own Instructions, Not My Code</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Tue, 14 Jul 2026 16:48:28 +0000</pubDate>
      <link>https://dev.to/magithar/the-bug-was-in-my-own-instructions-not-my-code-255p</link>
      <guid>https://dev.to/magithar/the-bug-was-in-my-own-instructions-not-my-code-255p</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/magithar/a-reader-pointed-out-my-ask-first-principle-only-covered-half-my-tool-so-i-fixed-it-345c"&gt;last article&lt;/a&gt;, a reader's comment exposed a hole in SKILLmama's design, and I shipped Phase 1.5 to fix it. This time, nobody had to point anything out to me. I did something I should have done a long time ago: I actually opened Antigravity, followed my own README, and watched what happened.&lt;/p&gt;

&lt;p&gt;It didn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Recording
&lt;/h2&gt;

&lt;p&gt;SKILLmama supports four agents — Claude Code, Claude.ai, OpenAI Codex, and Antigravity — and the README has always promised the same thing for all four: install it, then ask a capability question, and you get a scored, ranked recommendation with a compatibility/popularity/maintenance/simplicity breakdown.&lt;/p&gt;

&lt;p&gt;I had never actually verified that claim for Antigravity. So I opened it, typed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;find me the best vector database for a Python project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And got back a plain, generic answer. No stack scan. No constraint question. No scoring table. Just the kind of freeform response you'd get from any assistant with no skill loaded at all.&lt;/p&gt;

&lt;p&gt;SKILLmama wasn't running. It wasn't even trying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wrong Guesses
&lt;/h2&gt;

&lt;p&gt;My first assumption was reasonable: the &lt;code&gt;npx skills add&lt;/code&gt; CLI I'd been telling people to use must be installing to a path Antigravity doesn't check. I went and read the CLI's own source (&lt;code&gt;vercel-labs/skills&lt;/code&gt;) to find the real path. Found what looked like the answer. Copied the skill file there by hand. Restarted Antigravity. Tested again.&lt;/p&gt;

&lt;p&gt;Still nothing. Same generic answer.&lt;/p&gt;

&lt;p&gt;Second guess. Maybe I had the wrong directory entirely. I tried another path that seemed plausible from the CLI's agent config. Restarted again. Tested again.&lt;/p&gt;

&lt;p&gt;Still nothing.&lt;/p&gt;

&lt;p&gt;Two wrong guesses in a row are usually a sign you're debugging the wrong layer. I was reverse-engineering a &lt;em&gt;tool that manages the skill&lt;/em&gt;, when I should have been reading what the &lt;em&gt;agent itself&lt;/em&gt; actually documents about where it looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Answer Was in the Docs I Hadn't Read
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;antigravity.google/docs/skills&lt;/code&gt; — the actual, official documentation for how Antigravity discovers skills. I hadn't opened it once before this.&lt;/p&gt;

&lt;p&gt;It states, plainly, two locations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;Project&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;: &amp;lt;&lt;span class="n"&gt;workspace&lt;/span&gt;-&lt;span class="n"&gt;root&lt;/span&gt;&amp;gt;/.&lt;span class="n"&gt;agents&lt;/span&gt;/&lt;span class="n"&gt;skills&lt;/span&gt;/&amp;lt;&lt;span class="n"&gt;skill&lt;/span&gt;-&lt;span class="n"&gt;folder&lt;/span&gt;&amp;gt;/
&lt;span class="n"&gt;Global&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;:  ~/.&lt;span class="n"&gt;gemini&lt;/span&gt;/&lt;span class="n"&gt;config&lt;/span&gt;/&lt;span class="n"&gt;skills&lt;/span&gt;/&amp;lt;&lt;span class="n"&gt;skill&lt;/span&gt;-&lt;span class="n"&gt;folder&lt;/span&gt;&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither of my two guesses matched. The CLI's own source code — the thing I'd trusted as ground truth — pointed somewhere Antigravity was never going to look.&lt;/p&gt;

&lt;p&gt;I copied the skill to the real path, restarted Antigravity one more time, and asked it directly: &lt;strong&gt;"Which skills are installed?"&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SKILLmama — AI-Native Capability Discovery Engine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it was, listed under Global &amp;amp; Built-in Skills, sitting right next to Antigravity's own native commands. I asked the same vector-database question again — this time invoking it explicitly — and got back exactly what the README had been promising the whole time: a constraint question, then a scored breakdown.&lt;/p&gt;

&lt;p&gt;It worked. It had just never worked &lt;em&gt;the way I'd been telling people to install it.&lt;/em&gt;&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%2Fkmblgb3gp2lx8mmvfhoa.gif" 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%2Fkmblgb3gp2lx8mmvfhoa.gif" alt="Antigravity chat window showing the SKILLmama skill invoked to find a vector database for a Python project — it asks a clarifying constraint question, then returns a scored comparison table ranking candidates by compatibility, popularity, maintenance, and simplicity" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing the Loop
&lt;/h2&gt;

&lt;p&gt;Once I had a real repro, I checked whether anyone else had already run into this — and found an open issue on &lt;code&gt;vercel-labs/skills&lt;/code&gt; asking for the exact fix Antigravity actually needs. I added my repro to it: what the CLI installs today, what Antigravity actually reads, and how to reproduce the mismatch. Not my repo to fix, but worth leaving evidence for whoever does.&lt;/p&gt;

&lt;p&gt;The README now leads with the manual install to the correct path — confirmed, not assumed — and only mentions the CLI as a secondary option with an explicit warning about where it currently goes wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus: While I Was In There
&lt;/h2&gt;

&lt;p&gt;Chasing down the Antigravity bug surfaced two more things worth fixing in the same pass:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scoring got more honest.&lt;/strong&gt; The Compatibility score used to be inferred purely from your detected stack. Now, before scoring, SKILLmama checks your actual environment — does the required env var exist in &lt;code&gt;.env.example&lt;/code&gt;? Is the CLI on your PATH? A missing dependency now shows up as a flag on the score instead of a silently inflated number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four adapters had quietly drifted apart.&lt;/strong&gt; &lt;code&gt;codex/AGENTS.md&lt;/code&gt; and &lt;code&gt;antigravity/PROMPT.md&lt;/code&gt; still carried leftover wording from an earlier two-stage architecture that had since been dropped — meaning the instructions Codex and Antigravity were actually running had subtly diverged from the canonical pipeline in &lt;code&gt;skillmama/SKILL.md&lt;/code&gt;. Consolidated all three back to running identical logic, with only the genuinely agent-specific bits (like install syntax) left different.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;All of this is live in v1.4.3. For Antigravity specifically:&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="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.gemini/config/skills/skillmama
curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://raw.githubusercontent.com/Magithar/SKILLmama/main/skillmama/SKILL.md &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; ~/.gemini/config/skills/skillmama/SKILL.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Antigravity, then invoke it explicitly: &lt;code&gt;SKILLmama find me a vector database for this project&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;&lt;/strong&gt; — Apache 2.0.&lt;/p&gt;

&lt;p&gt;The lesson wasn't really about Antigravity. It was that "the README says it works" and "I watched it work" are two different claims, and I'd been treating the first one as good enough. It isn't. If you maintain something with install instructions for more than one platform, go run them yourself — not the happy path you remember writing, the actual one, today.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>antigravity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>10 Common Unity Networking Issues (and How to Fix Them)</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Mon, 06 Jul 2026 21:21:30 +0000</pubDate>
      <link>https://dev.to/magithar/10-common-unity-networking-issues-and-how-to-fix-them-55hg</link>
      <guid>https://dev.to/magithar/10-common-unity-networking-issues-and-how-to-fix-them-55hg</guid>
      <description>&lt;p&gt;Multiplayer bugs in Unity rarely look like networking bugs. They look like "the game froze," "the player teleported," or "it worked in the Editor and broke in the WebGL build." By the time you've traced it back to the actual cause, you've usually burned an afternoon.&lt;/p&gt;

&lt;p&gt;Here are 10 issues that show up constantly in Unity networking code (WebSocket-based, Socket.IO, or otherwise) with the actual root cause and the fix. A few of these come straight out of real regression tests and commit history in &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;socketio-unity&lt;/a&gt;, an MIT-licensed Socket.IO v4 client for Unity. The rest are patterns you'll recognize if you've shipped a multiplayer game.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Reconnect wipes your room/namespace state
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Connection drops for two seconds, comes back, and the player is no longer in their room/lobby/channel, even though the server never removed them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; A common (bad) reconnect implementation tears down the whole client and rebuilds it from scratch, including the list of channels/namespaces the player had joined. The reconnect "succeeds" at the transport level but silently drops application-level state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Reconnect logic should preserve subscriptions across the transport reset and only re-emit &lt;code&gt;join&lt;/code&gt;/&lt;code&gt;connect&lt;/code&gt; for namespaces the client already had open. If you're rebuilding the socket object on every reconnect attempt, stop. Reconnect the transport, keep the namespace map.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong: rebuilds everything, loses namespace state&lt;/span&gt;
&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnReconnect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateFreshEngine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Right: reuses the existing namespace map&lt;/span&gt;
&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnReconnect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ReconnectEngine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// _namespaces untouched&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. "get_gameObject can only be called from the main thread"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Random &lt;code&gt;UnityException&lt;/code&gt; thrown from inside a network event handler, but only sometimes, usually right when the server sends something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Your WebSocket/network library delivers callbacks on its own I/O thread. Any Unity API call (&lt;code&gt;transform.position =&lt;/code&gt;, &lt;code&gt;Instantiate&lt;/code&gt;, even some &lt;code&gt;Debug.Log&lt;/code&gt; paths) from that thread throws.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Never touch Unity APIs directly in a network callback. Queue the work and drain the queue from &lt;code&gt;Update()&lt;/code&gt; on the main thread.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnMessage&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;UnityMainThreadDispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Safe: runs on the main thread next Update()&lt;/span&gt;
        &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ParsePosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&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;This is table stakes for any Unity networking layer. If the library you're using doesn't do this for you automatically, you're going to hit this bug in production, not in testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;Connect()&lt;/code&gt; hangs with no timeout
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; State sits at &lt;code&gt;Connecting&lt;/code&gt; indefinitely. No error, no timeout, no exception. Just silence. Usually happens against a misconfigured server or a proxy that accepts TCP but drops the app-level handshake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Most networking code has a timeout for "can't reach the server" (connection refused) but not for "reached the server, and it's just... not responding." A raw TCP/WebSocket accept succeeding is not the same as the protocol handshake completing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Add an explicit handshake timeout that's separate from the reconnect backoff, and make sure it only applies to the &lt;em&gt;initial&lt;/em&gt; connect. Reconnect attempts should use their own backoff.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connectTimeoutMs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// abort with OnError(Timeout) if no handshake in 10s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't ship without a default. Set it to &lt;code&gt;0&lt;/code&gt; only if you have a specific reason to wait indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. WebGL builds behave nothing like the Editor
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Everything works in Editor and Standalone. WebGL build either doesn't connect, drops messages, or corrupts binary data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; WebGL doesn't have real threads or a real socket API. Your networking code has to bridge into browser JavaScript via a &lt;code&gt;.jslib&lt;/code&gt; plugin, which means a completely different code path (and completely different failure modes) than every other platform. String/byte marshalling between C# and JS is a common place for subtle bugs, for example framing a socket ID and message body with a delimiter character that can also appear &lt;em&gt;inside&lt;/em&gt; the message body.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Don't assume "works on Desktop" implies "works on WebGL." Test the WebGL build specifically, and when framing multi-part messages across the JS/C# boundary, pick a split strategy that can't be broken by message content. For example, split on the &lt;em&gt;first&lt;/em&gt; occurrence of a delimiter that's guaranteed not to appear in the ID (like a GUID), not by a fragile index heuristic.&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="c1"&gt;// Fragile: assumes id length is always under 40 chars&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&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="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Robust: split on first colon; GUIDs never contain one&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&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;var&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&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;idx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// safe even if msg itself contains colons&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. IL2CPP stripping deletes types that only exist at runtime
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; A &lt;code&gt;MonoBehaviour&lt;/code&gt; or class works perfectly in the Editor, then throws &lt;code&gt;MissingMethodException&lt;/code&gt; or silently no-ops in an IL2CPP build, but &lt;em&gt;only&lt;/em&gt; in the build, never in Play mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; IL2CPP's code stripper removes anything it can't prove is used, based on static analysis of scene/prefab references. If you create a component at runtime via &lt;code&gt;AddComponent&amp;lt;T&amp;gt;()&lt;/code&gt; or reflection rather than referencing it in a scene, the stripper doesn't see the reference and deletes it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Explicitly preserve runtime-created types in &lt;code&gt;link.xml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;linker&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;assembly&lt;/span&gt; &lt;span class="na"&gt;fullname=&lt;/span&gt;&lt;span class="s"&gt;"YourAssembly"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;fullname=&lt;/span&gt;&lt;span class="s"&gt;"YourNamespace.RuntimeCreatedComponent"&lt;/span&gt; &lt;span class="na"&gt;preserve=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/assembly&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/linker&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you add a new &lt;code&gt;MonoBehaviour&lt;/code&gt; that's instantiated dynamically (not dragged into a scene), add it to &lt;code&gt;link.xml&lt;/code&gt; in the same PR. This is the single most common "works everywhere except the build I'm about to ship" bug in Unity networking code.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. A malformed binary packet crashes the whole handler pipeline
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; One bad server message and the client stops processing &lt;em&gt;all&lt;/em&gt; subsequent binary events, or throws a &lt;code&gt;NullReferenceException&lt;/code&gt; that spams every frame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Binary protocols (file transfer, voice chat, custom serialization) often use a placeholder/reference scheme for large payloads. If a malformed or truncated packet is missing an expected field, code that assumes the field is always present throws. If that throw happens inside a per-frame processing loop instead of being caught at the boundary, it repeats forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Validate untrusted server input at the parsing boundary, not deep in application logic, and wrap binary assembly in a try/catch that routes to your normal error channel instead of letting the exception propagate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BinaryPacketAssembler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;placeholders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;OnError&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SocketError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ErrorType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="c1"&gt;// don't rethrow - one bad packet shouldn't kill the pipeline&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never assume a server-supplied packet is well-formed. Even from your own server, a proxy or version mismatch can produce malformed data.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. "Heartbeat timeout" that isn't actually a network problem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Client disconnects with a heartbeat/ping timeout error. You blame the network, check your server, find nothing wrong, and it happens again an hour later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; If your event handlers, once correctly dispatched to the main thread (see #2 for the threading model), do something slow like a big JSON parse, a physics query, or an asset load, they can block the main thread long enough that the ping/pong cycle misses its deadline. The client concludes the &lt;em&gt;connection&lt;/em&gt; died when actually the &lt;em&gt;game&lt;/em&gt; stalled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Before assuming it's a server/network issue, profile your event handlers for anything synchronous and slow. Keep handlers cheap; defer heavy work to a coroutine or the next frame instead of doing it inline in the callback.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Namespace/room auth fails silently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Client connects to the root namespace fine, but a sub-namespace/room never triggers its &lt;code&gt;connect&lt;/code&gt; event and no error appears anywhere obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Joining an authenticated namespace/channel usually requires passing an auth payload at connect time, not after. If you connect without it, or with an empty object, in many implementations the server just silently fails the namespace-level connect while the underlying transport stays fine. Nothing crashes, so nothing looks wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Always listen for a &lt;code&gt;connect_error&lt;/code&gt; on the namespace itself, and double-check you're passing auth at the point of connection, not as an afterthought:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong - no auth, silently rejected&lt;/span&gt;
&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/admin"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Right&lt;/span&gt;
&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sessionToken&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/admin"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;OnConnectError&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. Reflection-based config breaks silently when a dependency renames a field
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; A dedicated-server or multi-transport setup (for example, switching ports based on platform) works, then silently uses the wrong port after an unrelated dependency update. No compile error, no exception.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Code that sets configuration via reflection (&lt;code&gt;GetField&lt;/code&gt;/&lt;code&gt;GetProperty&lt;/code&gt; by string name, because the target type is defined in a third-party package you don't want a hard dependency on) is invisible to the compiler. If the third-party library renames a field from a public field to a property (or vice versa) in an update, your reflection call finds nothing, fails silently, and the fallback/default value gets used instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; If you must use reflection for soft dependencies, check &lt;em&gt;both&lt;/em&gt; field and property, and log loudly (not silently swallow) when neither is found. A silent fallback to a default value is exactly the kind of bug that survives in production for weeks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"port"&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="n"&gt;field&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&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="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"port"&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="n"&gt;prop&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&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="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Could not find 'port' field or property on &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - falling back to inspector value"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  10. Two reconnect timers running at once
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; After a rough network patch, you see reconnect attempts firing twice as often as configured, or backoff resetting unpredictably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; If your reconnect controller's &lt;code&gt;Start()&lt;/code&gt; isn't idempotent, calling it twice (for example, once from &lt;code&gt;OnDisconnect&lt;/code&gt; and once from a manual retry button, or once per re-entrant event) spins up a second timer loop instead of being a no-op.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Guard &lt;code&gt;Start()&lt;/code&gt; explicitly, and don't remove the guard during a refactor just because it looks redundant in the happy path. It's there for the unhappy path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Start&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="n"&gt;_enabled&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="c1"&gt;// idempotency guard - do not remove&lt;/span&gt;
    &lt;span class="n"&gt;_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;ScheduleNextAttempt&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;h2&gt;
  
  
  The common thread
&lt;/h2&gt;

&lt;p&gt;Almost none of these are "hard" bugs. They're boundary bugs. Main thread vs. network thread. Editor vs. IL2CPP build. Desktop vs. WebGL. Trusted code vs. untrusted server input. Every one of them is invisible until you cross the specific boundary that triggers it, which is exactly why they tend to surface in a build the day before a demo instead of in day-to-day Editor testing.&lt;/p&gt;

&lt;p&gt;If you're building Socket.IO-based multiplayer for Unity and would rather not rediscover these the hard way, &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;socketio-unity&lt;/a&gt; is an MIT-licensed Socket.IO v4 client that handles the main-thread dispatch, reconnect state preservation, WebGL bridge, and IL2CPP stripping concerns above out of the box, with a public test suite covering the regressions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install via UPM:&lt;/strong&gt; &lt;code&gt;https://github.com/Magithar/socketio-unity.git?path=/package&lt;/code&gt;&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>csharp</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A Reader Pointed Out My "Ask First" Principle Only Covered Half My Tool — So I Fixed It</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Tue, 30 Jun 2026 22:50:34 +0000</pubDate>
      <link>https://dev.to/magithar/a-reader-pointed-out-my-ask-first-principle-only-covered-half-my-tool-so-i-fixed-it-345c</link>
      <guid>https://dev.to/magithar/a-reader-pointed-out-my-ask-first-principle-only-covered-half-my-tool-so-i-fixed-it-345c</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/magithar/i-got-tired-of-asking-what-am-i-missing-so-i-made-my-ai-ask-first-g8"&gt;last article&lt;/a&gt;, I added Flow B to SKILLmama: run &lt;code&gt;/skillmama&lt;/code&gt; with no arguments, and it scans your project, finds capability gaps, and &lt;strong&gt;asks before it searches&lt;/strong&gt;. The whole point was to stop the AI from guessing what you need.&lt;/p&gt;

&lt;p&gt;Then &lt;a class="mentioned-user" href="https://dev.to/alexshev"&gt;@alexshev&lt;/a&gt; left this comment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Having the AI ask first is underrated because it changes the contract from answer-generation to ambiguity-reduction. The best agents I have used spend a little time shrinking the unknowns before they touch code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Ambiguity-reduction over answer-generation." That's a sharper framing of the idea than I'd come up with myself. But reading it back, I realized the comment had quietly exposed a hole in my own design.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hole
&lt;/h2&gt;

&lt;p&gt;SKILLmama has two entry points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flow B&lt;/strong&gt; — &lt;code&gt;/skillmama&lt;/code&gt; with no args → scan → &lt;strong&gt;ask&lt;/strong&gt; → search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flow A&lt;/strong&gt; — &lt;code&gt;/skillmama find me a job queue&lt;/code&gt; → scan → search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow B asked first. Flow A didn't.&lt;/p&gt;

&lt;p&gt;When you named a capability directly, SKILLmama scanned your project for stack context — and then went straight to searching and ranking. It reduced the ambiguity it could &lt;em&gt;see&lt;/em&gt; from your files, but it never reduced the ambiguity it couldn't: budget, license, self-hosted vs. hosted, "must work with what I already run."&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;/skillmama find me a job queue&lt;/code&gt; on a solo side project would happily return an enterprise-grade, infra-heavy #1 pick — technically the highest score, practically wrong for the person asking. The commenter's principle applied perfectly to Flow B. Flow A was still in answer-generation mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Phase 1.5
&lt;/h2&gt;

&lt;p&gt;I added one step to Flow A: &lt;strong&gt;Confirm Constraints.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phase 0   — parse the request
Phase 1   — scan the project
Phase 1.5 — if no constraints were stated: ask ONE informed question, then STOP   ← new
Phase 2   — derive search terms
Phase 3   — search
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key word is &lt;em&gt;informed&lt;/em&gt;. Because Phase 1 already scanned your stack, the question isn't a generic "any constraints?" — it's built from what it just found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SKILLmama: I see you're on Python / FastAPI / PostgreSQL / Docker / OpenAI.
Before I search — any constraints? (e.g. self-hosted, open-source only,
free tier, must integrate with PostgreSQL). Reply "none" to search with no filters.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it stops and waits — same hard stop that makes Flow B collaborative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Rules That Keep It From Being Annoying
&lt;/h2&gt;

&lt;p&gt;A clarifying question is only good if it doesn't fire when it shouldn't. So Phase 1.5 has guardrails:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It only fires when you gave no constraints.&lt;/strong&gt; If you already said &lt;code&gt;find me an open-source job queue&lt;/code&gt;, the constraint is on the table — it skips the question and searches immediately. No nagging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. It degrades gracefully.&lt;/strong&gt; If there are no project files to scan (empty folder, fresh repo), it doesn't print a broken &lt;code&gt;I see you're on [nothing]&lt;/code&gt; sentence. It falls back to a generic constraint question instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It asks once.&lt;/strong&gt; No re-prompting loops. One question, your answer (or "none"), then it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "One Informed Question" Beats "A Form"
&lt;/h2&gt;

&lt;p&gt;I could have made Flow A ask the same three questions Flow B asks. I didn't, on purpose.&lt;/p&gt;

&lt;p&gt;Flow B earns three questions because it's doing open-ended discovery — it found a handful of gaps and needs to know which ones matter, what your constraints are, and what it missed. Flow A already knows the capability. The only real unknown left is &lt;em&gt;constraints&lt;/em&gt;. Asking more than that would be friction for its own sake.&lt;/p&gt;

&lt;p&gt;The contract the commenter described — shrink the unknowns before you touch code — doesn't mean "ask everything." It means ask for the &lt;strong&gt;smallest input that most changes the output.&lt;/strong&gt; For a scored ranking, that input is constraints: a single "must be self-hosted" can reorder the entire top 3.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;Phase 1.5 is live in v1.4.0 across all four adapters — Claude Code, Claude.ai, OpenAI Codex, and Antigravity. Both entry points now reduce ambiguity before they act; they just ask the right number of questions for how much they already know.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add Magithar/SKILLmama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;&lt;/strong&gt; — Apache 2.0.&lt;/p&gt;

&lt;p&gt;And if you've got a sharp observation about where it still guesses instead of asking — leave a comment. The last one became a release.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>openai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Got Tired of Asking "What Am I Missing?" — So I Made My AI Ask First</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Tue, 30 Jun 2026 20:11:39 +0000</pubDate>
      <link>https://dev.to/magithar/i-got-tired-of-asking-what-am-i-missing-so-i-made-my-ai-ask-first-g8</link>
      <guid>https://dev.to/magithar/i-got-tired-of-asking-what-am-i-missing-so-i-made-my-ai-ask-first-g8</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/magithar/stop-guessing-which-library-to-use-i-built-an-ai-capability-discovery-engine-3p7p"&gt;first article&lt;/a&gt;, I built SKILLmama: an AI skill that finds, scores, and ranks the best library for your exact stack.&lt;/p&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/magithar/a-high-score-means-nothing-if-the-tool-is-dangerous-so-i-added-a-security-gate-3hpc"&gt;second article&lt;/a&gt;, I added a security gate — because a high score means nothing if the tool is dangerous.&lt;/p&gt;

&lt;p&gt;Both of those workflows had the same assumption baked in: &lt;strong&gt;you already know what you're looking for.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You type &lt;code&gt;/skillmama find me a job queue for Node.js&lt;/code&gt; and it searches, scores, and returns ranked picks. That's great when you have a specific gap in mind.&lt;/p&gt;

&lt;p&gt;But most of the time, you don't. You're deep in building something and you haven't stopped to ask: &lt;em&gt;do I have auth? do I have observability? do I have a queue?&lt;/em&gt; You find out the hard way — when a feature ships without rate limiting, or when something breaks and there's no error tracking.&lt;/p&gt;

&lt;p&gt;So I flipped the workflow. In v1.3, &lt;code&gt;/skillmama&lt;/code&gt; with no arguments scans your project first and asks what to find.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Flow vs. The New Flow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before (Flow A — still exists):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/skillmama find me an auth library for Next.js
    ↓
scan project for stack context
    ↓
search → score → recommend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Now (Flow B — triggered when no capability is given):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/skillmama
    ↓
deep scan: reads package files, config, source structure
    ↓
gap analysis: what's missing for this type of project?
    ↓
ask 3 questions — STOP and wait for your answer
    ↓
you pick a gap → search → score → recommend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both flows converge at the same search and scoring pipeline. Flow B just adds the scan and Q&amp;amp;A in front.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Scan Actually Does
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;/skillmama&lt;/code&gt; with no arguments, it reads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Package files (&lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;go.mod&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Config and infra (&lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;docker-compose.yml&lt;/code&gt;, &lt;code&gt;.env.example&lt;/code&gt;, &lt;code&gt;vercel.json&lt;/code&gt;, &lt;code&gt;fly.toml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;README.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Source file structure (file listing, not full reads)&lt;/li&gt;
&lt;li&gt;2–4 representative source files — the entry point, a route handler, a model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From that, it builds a &lt;strong&gt;Stack Profile&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Language:      TypeScript
Framework:     Next.js 14
Database:      Postgres via Prisma
Auth:          none detected
Caching:       none detected
AI/LLM:        OpenAI SDK
Queue/Jobs:    none detected
Search:        none detected
Storage:       none detected
Email:         none detected
Payments:      none detected
Observability: none detected
Testing:       Vitest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gap Analysis: Not Everything Missing Is a Problem
&lt;/h2&gt;

&lt;p&gt;Once the Stack Profile is built, SKILLmama doesn't just flag every empty row as a gap. It looks at what the project is actually doing and assigns severity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High&lt;/strong&gt; — typical for this type of app, likely needed soon&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium&lt;/strong&gt; — useful but not urgent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low&lt;/strong&gt; — speculative / nice-to-have&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a Next.js app with OpenAI already wired up, the output might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## SKILLmama — Project Scan&lt;/span&gt;

Stack detected: TypeScript / Next.js 14 / Postgres / OpenAI / no auth

Capability gaps found:

| # | Gap              | Severity | Why it matters for your stack                              |
|---|------------------|----------|------------------------------------------------------------|
| 1 | Authentication   | High     | No auth detected — any route is publicly accessible        |
| 2 | Rate limiting    | High     | OpenAI calls with no rate limiting = unbounded cost risk   |
| 3 | Observability    | Medium   | No error tracking — you'll find out about failures late    |
| 4 | Email sending    | Medium   | Common for Next.js apps with user accounts                 |
| 5 | Vector/RAG layer | Low      | OpenAI is present but no vector store — may be intentional |

A few quick questions before I search:
&lt;span class="p"&gt;1.&lt;/span&gt; Which gap(s) would you like me to find options for?
&lt;span class="p"&gt;2.&lt;/span&gt; Any constraints? (self-hosted, open-source, must have MCP support, etc.)
&lt;span class="p"&gt;3.&lt;/span&gt; Anything I missed about your project or plans?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it &lt;strong&gt;stops&lt;/strong&gt;. It does not proceed until you reply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Hard Stop Matters
&lt;/h2&gt;

&lt;p&gt;The first version of this flow I sketched out just picked the highest-severity gap automatically and ran with it. That was wrong for two reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Severity is relative to intent.&lt;/strong&gt; A High gap for a consumer SaaS is a Low gap for an internal tool. The scanner doesn't know which one you're building — you do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. You might want to address multiple gaps in order.&lt;/strong&gt; If you reply "1 and 2", SKILLmama runs the full search and scoring pipeline once per gap and presents each as its own results block. You get ranked picks for auth &lt;em&gt;and&lt;/em&gt; rate limiting in one session, not just the first one.&lt;/p&gt;

&lt;p&gt;The hard stop is what makes Flow B collaborative rather than presumptuous.&lt;/p&gt;

&lt;h2&gt;
  
  
  After You Answer
&lt;/h2&gt;

&lt;p&gt;Once you reply — say "1 and 3, open-source preferred" — SKILLmama sets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;capability&lt;/code&gt; = Authentication, Observability&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;constraints&lt;/code&gt; = open-source preferred&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And jumps straight to Phase 2 (deriving search terms). It &lt;strong&gt;skips Phase 0 and Phase 1&lt;/strong&gt; entirely — the stack is already known from the scan, so there's nothing left to parse.&lt;/p&gt;

&lt;p&gt;From there, the same pipeline runs as always:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phase 2 — derive search terms
Phase 3 — 5-tier search (GitHub → MCP → npm/PyPI → Templates)
Phase 3.5 — security gate (libraries)
Phase 3.6 — companion skills search
Phase 3.7 — security gate (skills)
Phase 4 — score each candidate
Phase 5 — present results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the auth gap on a Next.js + Postgres stack, that produces something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Scoring all candidates against TypeScript / Next.js 14 / Postgres:

| Candidate   | Compat | Pop | Maint | Simple | Score    |
|-------------|--------|-----|-------|--------|----------|
| NextAuth.js | 10     | 9   | 9     | 9      | &lt;span class="gs"&gt;**9.55**&lt;/span&gt; |
| Lucia        | 9      | 7   | 10    | 8      | &lt;span class="gs"&gt;**8.75**&lt;/span&gt; |
| Clerk        | 8      | 8   | 10    | 10     | &lt;span class="gs"&gt;**8.90**&lt;/span&gt; |

&lt;span class="gh"&gt;#1 — NextAuth.js · Score: 9.55/10&lt;/span&gt;
The de facto auth layer for Next.js — first-party adapter, zero config for most providers.
&lt;span class="p"&gt;-&lt;/span&gt; Compatibility: 10/10 — built for Next.js, Prisma adapter available
&lt;span class="p"&gt;-&lt;/span&gt; Popularity:     9/10 — 22k stars, 800k npm downloads/week
&lt;span class="p"&gt;-&lt;/span&gt; Maintenance:    9/10 — committed 1 week ago
&lt;span class="p"&gt;-&lt;/span&gt; Simplicity:    9/10  — provider config only, Prisma adapter is one import
&lt;span class="p"&gt;-&lt;/span&gt; Security:      PASS
&lt;span class="p"&gt;-&lt;/span&gt; Install: npm install next-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Changed in the Adapters
&lt;/h2&gt;

&lt;p&gt;Flow B is live across all four adapters:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Adapter&lt;/th&gt;
&lt;th&gt;How to trigger Flow B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/skillmama&lt;/code&gt; (no args)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude.ai&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/skillmama&lt;/code&gt; (no args)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Codex&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"scan my project and tell me what I need"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Antigravity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"scan my project and tell me what I need"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Any agent (via skills CLI):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add Magithar/SKILLmama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add Magithar/SKILLmama &lt;span class="nt"&gt;-a&lt;/span&gt; claude-code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-a claude-code&lt;/code&gt; flag matters: without it, non-interactive installs can silently skip wiring the skill into &lt;code&gt;.claude/skills/&lt;/code&gt;, and &lt;code&gt;/skillmama&lt;/code&gt; won't show up. Or copy the file directly:&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="nb"&gt;cp&lt;/span&gt; .claude/commands/skillmama.md /your-project/.claude/commands/skillmama.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;/skillmama&lt;/code&gt; in any Claude Code session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude.ai:&lt;/strong&gt; Clone the repo, run &lt;code&gt;zip -r skillmama.zip skillmama/&lt;/code&gt;, then upload the zip under Customize → Skills.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI Codex:&lt;/strong&gt; Place &lt;code&gt;codex/AGENTS.md&lt;/code&gt; in your repo root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Antigravity:&lt;/strong&gt; Load &lt;code&gt;antigravity/PROMPT.md&lt;/code&gt; as system prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Repo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Apache 2.0. If you used v1.0, v1.1, or v1.2 — pull the latest. The only new behavior is what happens when you run &lt;code&gt;/skillmama&lt;/code&gt; with nothing after it.&lt;/p&gt;

&lt;p&gt;Found a gap category that should be in the scanner? Open an &lt;a href="https://github.com/Magithar/SKILLmama/issues" rel="noopener noreferrer"&gt;issue&lt;/a&gt; — the list is meant to grow with real project types.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>openai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A High Score Means Nothing If the Tool Is Dangerous — So I Added a Security Gate</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Mon, 29 Jun 2026 12:41:19 +0000</pubDate>
      <link>https://dev.to/magithar/a-high-score-means-nothing-if-the-tool-is-dangerous-so-i-added-a-security-gate-3hpc</link>
      <guid>https://dev.to/magithar/a-high-score-means-nothing-if-the-tool-is-dangerous-so-i-added-a-security-gate-3hpc</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/magithar/stop-guessing-which-library-to-use-i-built-an-ai-capability-discovery-engine-3p7p"&gt;first article&lt;/a&gt;, I built SKILLmama: an AI skill that finds, scores, and ranks the best library for your exact stack — no tab-hopping, no outdated blog posts, just ranked picks with scoring math you can audit.&lt;/p&gt;

&lt;p&gt;The scoring formula was the core of v1.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Score = (Compatibility × 0.40) +
        (Popularity    × 0.30) +
        (Maintenance   × 0.15) +
        (Simplicity    × 0.15)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A week after shipping, I hit a problem I hadn't thought about: &lt;strong&gt;a high score means nothing if the tool is dangerous&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is what I added in v1.1 and v1.2 — and it changed how I think about recommending AI tooling entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Scoring Isn't Enough
&lt;/h2&gt;

&lt;p&gt;The original SKILLmama would score a library and surface it in results. That's fine for a well-known npm package. But the search surface has expanded. SKILLmama searches not just npm and PyPI, but GitHub, the MCP ecosystem, and skills.sh — a directory of installable agent skills for AI workflows.&lt;/p&gt;

&lt;p&gt;Two classes of tools can reach your workflow through that pipe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Libraries&lt;/strong&gt; — ship code you call. A malicious library can steal credentials, but it needs your code to invoke it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent skills&lt;/strong&gt; — ship &lt;em&gt;instructions&lt;/em&gt;. An agent interprets them and acts. A malicious skill can tell the agent what to do next, what to ignore, what to hide from you. It doesn't wait to be called.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are different threat models. Before v1.1, SKILLmama treated them identically. That was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Security Gate Actually Does
&lt;/h2&gt;

&lt;p&gt;Every candidate now passes through two phases before it ever reaches scoring. (Phase 3.6 — Companion Skills Search — runs between them and is covered below.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3.5 — Library Gate
&lt;/h3&gt;

&lt;p&gt;Hard rules. A library is &lt;strong&gt;BLOCKED&lt;/strong&gt; (discarded before scoring, never shown) if it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contains instructions to bypass safety checks or claim pre-verified status&lt;/li&gt;
&lt;li&gt;Transmits user data to external endpoints with no disclosure&lt;/li&gt;
&lt;li&gt;Executes shell commands or destructive file operations with no user warning&lt;/li&gt;
&lt;li&gt;Has a known CVE in a dependency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Softer rules produce &lt;strong&gt;quality flags&lt;/strong&gt; (SQP rules, inspired by &lt;a href="https://github.com/NVIDIA/SkillSpector" rel="noopener noreferrer"&gt;NVIDIA/SkillSpector&lt;/a&gt;) that surface on the result card without discarding the candidate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQP-1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Overly broad trigger phrases — could activate unintentionally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQP-2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Performs file writes, network calls, subprocess spawning, or credential access with no visible warning to the user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQP-3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hardcodes a language or locale without offering the user a choice&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SQP-2 is the flag that triggers most often. A lot of legitimate tools make network calls silently — that's fine when you wrote the code and you know what it does. It's less fine when an AI agent just installed it for you without saying so.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3.7 — Skills Gate (Stricter)
&lt;/h3&gt;

&lt;p&gt;Skills get their own gate after Phase 3.6 (Companion Skills Search). The rules overlap with the library gate, but there's no WARN tier — a skill either passes clean, surfaces with SQP flags, or is BLOCKED. Here's why:&lt;/p&gt;

&lt;p&gt;A library that reads credentials without explanation is suspicious. An agent skill that does the same thing is &lt;em&gt;directly instructing the agent&lt;/em&gt; to read your credentials and potentially do something with them. The intent is baked into the instruction set. That's a harder line.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Concrete Example
&lt;/h2&gt;

&lt;p&gt;Let's say SKILLmama is finding a job queue for a Node.js project. During Tier 3 (MCP Ecosystem) and Phase 3.6 (Companion Skills Search), it surfaces a hypothetical skill called &lt;code&gt;queue-manager-skill&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's what the gate might flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phase 3.7 — Evaluating: queue-manager-skill

Trigger phrase: "whenever I need to manage tasks"
→ SQP-1: overly broad — no exclusion conditions, could fire on unrelated requests

On each job completion:
  POST job_result to https://analytics.queuemanager-cloud.io
→ SQP-2: network call with no user disclosure

Result: ⚠️ SQP-1, SQP-2 — surfaces in Companion Skills with flags visible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill isn't blocked — it might be legitimate, and you might decide the analytics endpoint is fine. But you see it &lt;em&gt;before&lt;/em&gt; installing anything. That's the point.&lt;/p&gt;

&lt;p&gt;Compare to a library result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#1 — BullMQ · Score: 9.10/10
Redis-backed job queue, official Node.js SDK, 15k GitHub stars.
- Compatibility: 10/10 — native Node/TypeScript, full Express integration
- Popularity:     9/10 — 15k stars, 1.2M npm downloads/week
- Maintenance:   10/10 — committed 3 days ago
- Simplicity:    8/10  — Redis required, well-documented setup
- Security:      PASS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PASS&lt;/code&gt; means it cleared all hard gates, no SQP flags. You can install it without reading fine print.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Output Doesn't Shout About It
&lt;/h2&gt;

&lt;p&gt;One deliberate decision: Phases 3.5 and 3.7 don't appear as sections in the output. There's no "Security Report" block. Security findings appear &lt;em&gt;inline on each candidate card&lt;/em&gt; only.&lt;/p&gt;

&lt;p&gt;The reason: if security is a separate section, developers scroll past it. If it's on the card, it's part of the decision — you see the score and the security line together. A &lt;code&gt;PASS&lt;/code&gt; blends into the card. An &lt;code&gt;⚠️ SQP-2&lt;/code&gt; stands out where it matters.&lt;/p&gt;

&lt;p&gt;Blocked candidates are silently discarded — they never appear in results, never in "Also Considered." If something is genuinely dangerous, you don't need to know it lost; you just don't see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Companion Skills: Why This Became Necessary
&lt;/h2&gt;

&lt;p&gt;In v1.2, SKILLmama added Phase 3.6: after finding library candidates, it also searches skills.sh and GitHub for installable agent skills that pair with top picks.&lt;/p&gt;

&lt;p&gt;For the vector database example from the first article, that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Companion Skills:
- qdrant-memory-skill (skills.sh) — Security: PASS
  Adds Qdrant as persistent memory for your AI agent; install alongside qdrant-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the stricter gate on skills stopped being theoretical. Once SKILLmama was actively surfacing skills alongside libraries, it needed to actually evaluate them — not assume that anything on skills.sh was safe by virtue of being listed there. The ecosystem is too new for that assumption to hold.&lt;/p&gt;

&lt;p&gt;The threshold I landed on: anything that an agent will &lt;em&gt;interpret as instructions&lt;/em&gt; gets a harder gate than code you call yourself. You can audit code. Instructions are harder to reason about at a glance — that asymmetry is why the gate is stricter.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;Full output for &lt;code&gt;/skillmama find me a job queue for my Node.js + Redis project&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scoring all candidates against Node.js / Redis / Express:

#1 — BullMQ · Score: 9.10/10
Redis-backed queue with full TypeScript support and battle-tested at scale.
- Compatibility: 10/10 — built for Node/Redis, native TypeScript
- Popularity:     9/10 — 15k stars, 1.2M downloads/week
- Maintenance:   10/10 — committed 3 days ago
- Simplicity:    8/10  — Redis required, excellent docs
- Security:      PASS
- Install:        npm install bullmq

#2 — bee-queue · Score: 7.85/10
Lighter alternative; fewer features, faster setup.
- Compatibility:  9/10 — Node/Redis native
- Popularity:     6/10 — 3.5k stars, 180k downloads/week
- Maintenance:    8/10 — committed 2 weeks ago
- Simplicity:    9/10  — minimal config, fast local setup
- Security:      PASS

Also Considered: Agenda (MongoDB-based, no Redis dep), p-queue (in-process only)

Companion Skills:
- bullmq-agent-skill · ⚠️ SQP-2 — performs Redis writes with no user confirmation prompt
  Automates job scheduling from natural language; review before installing

Next Steps:
1. npm install bullmq and spin up Redis via docker run redis to validate locally
2. If you want to skip Redis infra, evaluate Agenda — MongoDB-native
3. Review the SQP-2 flag on bullmq-agent-skill before installing in any automated pipeline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Any agent (via skills CLI):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add Magithar/SKILLmama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add Magithar/SKILLmama &lt;span class="nt"&gt;-a&lt;/span&gt; claude-code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-a claude-code&lt;/code&gt; flag matters: without it, non-interactive installs can silently skip wiring the skill into &lt;code&gt;.claude/skills/&lt;/code&gt;, and &lt;code&gt;/skillmama&lt;/code&gt; won't show up. Or copy the file directly:&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="nb"&gt;cp&lt;/span&gt; .claude/commands/skillmama.md /your-project/.claude/commands/skillmama.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;/skillmama&lt;/code&gt; in any Claude Code session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude.ai:&lt;/strong&gt; Clone the repo, run &lt;code&gt;zip -r skillmama.zip skillmama/&lt;/code&gt;, then upload the zip under Customize → Skills.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI Codex:&lt;/strong&gt; Place &lt;code&gt;codex/AGENTS.md&lt;/code&gt; in your repo root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Antigravity:&lt;/strong&gt; Load &lt;code&gt;antigravity/PROMPT.md&lt;/code&gt; as system prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Repo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Apache 2.0. The security gate is live across all four adapters. If you used v1.0, pull the latest. The only user-visible changes are the &lt;code&gt;Security:&lt;/code&gt; line on each result card and a Companion Skills section when agent skills are found.&lt;/p&gt;

&lt;p&gt;Security findings from real use? Drop them in the &lt;a href="https://github.com/Magithar/SKILLmama/issues" rel="noopener noreferrer"&gt;issues&lt;/a&gt; — the SQP ruleset is meant to grow.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>antigravity</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A Deterministic Scoring Engine That Finds the Best Library for Your Exact Stack</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Fri, 19 Jun 2026 02:17:07 +0000</pubDate>
      <link>https://dev.to/magithar/stop-guessing-which-library-to-use-i-built-an-ai-capability-discovery-engine-3p7p</link>
      <guid>https://dev.to/magithar/stop-guessing-which-library-to-use-i-built-an-ai-capability-discovery-engine-3p7p</guid>
      <description>&lt;p&gt;Every developer knows this feeling.&lt;/p&gt;

&lt;p&gt;You need a vector database. Or a job queue. Or a .dwg parser for Node.js. You open a browser tab, search GitHub, open five more tabs, try to compare stars and last-commit dates, get distracted by a Medium post from 2019, and 45 minutes later you've picked something based on vibes.&lt;/p&gt;

&lt;p&gt;There's a better way. I built it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SKILLmama&lt;/strong&gt; finds, scores, and ranks the best library, SDK, or tool for your exact stack — no tab-hopping, no outdated blog posts, just ranked picks with scoring math you can audit.&lt;/p&gt;

&lt;p&gt;It works with Claude Code, Claude.ai, OpenAI Codex, and Antigravity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With How We Pick Libraries
&lt;/h2&gt;

&lt;p&gt;Most library selection looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Google "best [thing] for [framework]"&lt;/li&gt;
&lt;li&gt;Find a Reddit thread from 3 years ago&lt;/li&gt;
&lt;li&gt;Pick the one with the most upvotes&lt;/li&gt;
&lt;li&gt;Discover 6 months later that it hasn't been maintained since 2021&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The real question isn't "what's popular" — it's "what fits &lt;em&gt;my&lt;/em&gt; stack, has active maintenance, won't take a week to integrate, and is popular enough to have community support?" Those are four separate signals, and you need all four weighted correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scoring Formula
&lt;/h2&gt;

&lt;p&gt;SKILLmama scores every candidate on four dimensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Score = (Compatibility × 0.40) +
        (Popularity    × 0.30) +
        (Maintenance   × 0.15) +
        (Simplicity    × 0.15)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compatibility&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;Language/framework fit, official SDK, integration effort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Popularity&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;GitHub stars, npm/PyPI weekly downloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;Days since last commit, release cadence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simplicity&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;Setup effort, documentation quality&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compatibility is weighted highest because a library built for Java won't help you in a Python project, no matter how popular it is. Popularity comes second because the ecosystem around a library matters. Maintenance and simplicity round it out.&lt;/p&gt;

&lt;p&gt;Every score is 1–10 per dimension. No black box. You can see exactly why something ranked #1.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-Tier Search Hierarchy
&lt;/h2&gt;

&lt;p&gt;SKILLmama doesn't just search one place. It works through five tiers in order, stopping when it has 8+ candidates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;What it finds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;skills.sh&lt;/td&gt;
&lt;td&gt;Reusable skills and capability patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;td&gt;Open-source libraries, frameworks, SDKs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Smithery / MCP Ecosystem&lt;/td&gt;
&lt;td&gt;AI-native tools installable as MCP servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;npm / PyPI / pkg.go.dev&lt;/td&gt;
&lt;td&gt;Package registries with download signals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Curated Templates&lt;/td&gt;
&lt;td&gt;LangGraph, OpenHands, cookbook examples&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tier 3 is the interesting one. The MCP ecosystem is growing fast — if there's an MCP server for your capability, you might be able to plug it directly into your AI workflow instead of writing integration code. SKILLmama surfaces that option explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Security Gate
&lt;/h2&gt;

&lt;p&gt;But a high score means nothing if the tool is unsafe. Before any candidate gets scored, it passes through a two-layer security and quality check.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;hard gate&lt;/strong&gt; discards candidates that are genuinely unsafe: CVE dependencies, undisclosed data exfiltration, jailbreak instructions, or destructive operations with no user warning. These never reach the scoring phase.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;quality flags&lt;/strong&gt; (SQP rules, inspired by &lt;a href="https://github.com/NVIDIA/SkillSpector" rel="noopener noreferrer"&gt;NVIDIA/SkillSpector&lt;/a&gt;) surface softer issues without discarding the candidate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQP-1&lt;/strong&gt; — Vague trigger phrases that could activate unintentionally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQP-2&lt;/strong&gt; — Missing warnings on file writes, network calls, or credential access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQP-3&lt;/strong&gt; — Hardcoded language or locale without user opt-in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every result card shows a Security line: &lt;code&gt;PASS&lt;/code&gt;, &lt;code&gt;⚠️ SQP-2 — [finding]&lt;/code&gt;, or &lt;code&gt;🚫 BLOCKED&lt;/code&gt;. You always know what you're installing. No surprises. No silent risks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                      USER REQUEST                       │
└─────────────────────────┬───────────────────────────────┘
                          │
                          ▼
              ┌───────────────────────┐
              │   PHASE 0             │
              │   Understand Request  │
              │   Extract: capability,│
              │   stack, constraints  │
              └───────────┬───────────┘
                          │
                          ▼
                   ◇ Capability
                     vague?
                   /         \
                 YES           NO
                  │             │
                  ▼             │
          Ask 1 clarifying      │
          question, await       │
          user response         │
                  │             │
                  └──────┬──────┘
                         │
                         ▼
              ┌───────────────────────┐
              │   PHASE 1             │
              │   Architecture Scan   │
              └───────────┬───────────┘
                          │
                          ▼
                   ◇ In a project
                     repo?
                   /         \
                 YES           NO
                  │             │
                  ▼             │
        Read: package.json,     │
        Dockerfile, README,     │
        source files            │
                  │             │
                  └──────┬──────┘
                         │
                         ▼
              ┌───────────────────────┐
              │   PHASE 2             │
              │   Capability Gap      │
              │   Detection           │
              │                       │
              │   Define:             │
              │   CAPABILITY          │
              │   STACK               │
              │   CONSTRAINTS         │
              │   SEARCH_TERMS (3–5)  │
              └───────────┬───────────┘
                          │
                          ▼
              ┌───────────────────────┐
              │   PHASE 3             │
              │   5-Tier Search       │
              └───────────┬───────────┘
                          │
              Tier 1 ── skills.sh
                 ↓
              Tier 2 ── GitHub (stars, recency, contrib)
                 ↓
              Tier 3 ── MCP Ecosystem
                 ↓
              Tier 4 ── npm / PyPI registries
                 ↓
              Tier 5 ── Templates &amp;amp; Cookbooks
                          │
                   ◇ 8+ candidates found?
                   /              \
                 YES               NO
                  │                │
            Skip remaining    Continue tiers
            tiers                  │
                  └──────┬─────────┘
                         │
                         ▼
              ┌────────────────────────────────────────┐
              │   PHASE 3.5 — Security &amp;amp; Quality Gate  │
              │                                        │
              │   Hard Gate:                           │
              │   🚫 BLOCKED → discard, never score    │
              │   ⚠️  WARN   → show, user confirms     │
              │                                        │
              │   Quality flags (SQP rules):           │
              │   SQP-1  Vague triggers                │
              │   SQP-2  Missing user warnings         │
              │   SQP-3  Policy violations             │
              └───────────────┬────────────────────────┘
                              │
                              ▼
              ┌────────────────────────────────────────┐
              │   PHASE 4 — Score Each Candidate       │
              │                                        │
              │   Score = (C × 0.40) +                 │
              │           (P × 0.30) +                 │
              │           (M × 0.15) +                 │
              │           (S × 0.15)                   │
              └───────────────┬────────────────────────┘
                              │
                              ▼
              ┌────────────────────────────────────────┐
              │   PHASE 5 — Present Results            │
              │                                        │
              │   #1, #2, #3 — full score breakdown    │
              │   Also Considered — table              │
              │   MCP callout (if found)               │
              │   Next Steps (3 actions)               │
              └────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  End-to-End Example
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/skillmama find me a vector database for my FastAPI + Python project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Phase 1 — Architecture Scan&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SKILLmama reads your project files first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ pyproject.toml  → Python 3.11, FastAPI, SQLAlchemy
✓ Dockerfile      → containerized, no GPU
✓ .env.example    → OPENAI_API_KEY present → RAG use case confirmed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detected stack: &lt;code&gt;Python / FastAPI / PostgreSQL / Docker / OpenAI&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — Capability Gap Detection&lt;/strong&gt; &lt;em&gt;(runs internally)&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CAPABILITY : vector database for RAG / semantic search
STACK      : Python / FastAPI / Docker / OpenAI
CONSTRAINTS: containerizable, Python client, active maintenance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Phase 3 — 5-Tier Search&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tier 1 skills.sh  → "qdrant-memory-skill", "chroma-rag-skill"
Tier 2 GitHub     → qdrant (17k★), chroma (14k★), pgvector (11k★), milvus (29k★)
Tier 3 MCP        → qdrant-mcp-server, chroma-mcp
Tier 4 PyPI       → qdrant-client (380k/wk), chromadb (620k/wk), pgvector (180k/wk)
Tier 5 Templates  → LangChain + Qdrant RAG template, FastAPI + Chroma starter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Phase 4 — Scoring&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;Compat&lt;/th&gt;
&lt;th&gt;Popular&lt;/th&gt;
&lt;th&gt;Maint&lt;/th&gt;
&lt;th&gt;Simple&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qdrant&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.05&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pgvector&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.65&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chroma&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.55&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Milvus&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.05&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Phase 5 — Output&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#1 — Qdrant · Score: 9.05/10
Docker-native, official Python client, active MCP server for AI workflows.
- Compatibility: 9/10  — official qdrant-client SDK, FastAPI examples in docs
- Popularity:    8/10  — 17k GitHub stars, 380k PyPI downloads/week
- Maintenance:  10/10  — committed 2 days ago, weekly releases
- Simplicity:   9/10  — docker run + pip install, 5-min setup
- Security:     PASS  — no findings
- Install: docker run -p 6333:6333 qdrant/qdrant &amp;amp;&amp;amp; pip install qdrant-client

#2 — pgvector · Score: 8.65/10
Stay on PostgreSQL — no new infra, native SQL queries.

#3 — Chroma · Score: 8.55/10
Easiest local dev setup; best for prototyping before scaling.

MCP Option: qdrant-mcp-server — install as MCP tool for direct AI memory integration.

Next Steps:
1. docker run qdrant/qdrant and pip install qdrant-client to validate locally
2. Use the LangChain + Qdrant RAG template as a starting point
3. If staying Postgres-only, evaluate pgvector — saves an infra hop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's 45 minutes of tab-hopping, compressed into a structured decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install in 4 AI Systems
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&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="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /your-project/.claude/commands
&lt;span class="nb"&gt;cp&lt;/span&gt; .claude/commands/skillmama.md /your-project/.claude/commands/skillmama.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then type &lt;code&gt;/skillmama&lt;/code&gt; in any Claude Code session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude.ai:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clone the repo&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zip -r skillmama.zip skillmama/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Go to Customize → Skills → + and upload the zip&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;/skillmama&lt;/code&gt; in any conversation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;OpenAI Codex:&lt;/strong&gt;&lt;br&gt;
Place &lt;code&gt;codex/AGENTS.md&lt;/code&gt; in your repo root, then ask naturally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex &lt;span class="s2"&gt;"find me the best job queue for this project"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Antigravity:&lt;/strong&gt;&lt;br&gt;
Load &lt;code&gt;antigravity/PROMPT.md&lt;/code&gt; as the system prompt, then ask naturally.&lt;/p&gt;

&lt;p&gt;All four adapters run the same pipeline and produce the same output format.&lt;/p&gt;


&lt;h2&gt;
  
  
  What SKILLmama Is Not
&lt;/h2&gt;

&lt;p&gt;Not an IDE. Not autocomplete. Not a chatbot.&lt;/p&gt;

&lt;p&gt;It's a &lt;strong&gt;capability oracle&lt;/strong&gt; — it tells you what to use and why, with evidence. You still write the code. SKILLmama just makes sure you're writing it with the right tool.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Repo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Apache 2.0 licensed. Works with Claude Code, Claude.ai, OpenAI Codex, and Antigravity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/skillmama find me a vector database for my FastAPI project
/skillmama what auth library should I use for my Next.js app?
/skillmama scan my project and tell me what's missing
/skillmama find a .dwg parser for Node.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Try it on your next project: &lt;a href="https://github.com/Magithar/SKILLmama" rel="noopener noreferrer"&gt;github.com/Magithar/SKILLmama&lt;/a&gt;. If you've ever spent an afternoon picking the wrong library, I'd love to hear what you wish you'd known sooner.&lt;/p&gt;

&lt;p&gt;If SKILLmama saves you time, a ⭐ on the repo goes a long way — it helps other developers find it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>antigravity</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Game Identity Registry Pattern — Bridging Mirror netId and Socket.IO playerId</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Sun, 03 May 2026 00:05:04 +0000</pubDate>
      <link>https://dev.to/magithar/the-gameidentityregistry-pattern-bridging-mirror-netid-and-socketio-playerid-45lg</link>
      <guid>https://dev.to/magithar/the-gameidentityregistry-pattern-bridging-mirror-netid-and-socketio-playerid-45lg</guid>
      <description>&lt;p&gt;When you run Mirror and Socket.IO in the same Unity project, you immediately hit a translation problem.&lt;/p&gt;

&lt;p&gt;Mirror identifies players by &lt;code&gt;netId&lt;/code&gt; — a &lt;code&gt;uint&lt;/code&gt; assigned at spawn time by the Mirror host. Socket.IO identifies players by &lt;code&gt;playerId&lt;/code&gt; — a &lt;code&gt;string&lt;/code&gt; assigned by your Node.js backend when they connect.&lt;/p&gt;

&lt;p&gt;These two IDs have nothing to do with each other. They're generated by different systems at different times. But when a &lt;code&gt;score_update&lt;/code&gt; arrives from your Socket.IO backend with a &lt;code&gt;playerId&lt;/code&gt;, you need to find the corresponding Mirror &lt;code&gt;NetworkIdentity&lt;/code&gt; to apply the effect. And when a Mirror player spawns, you need to register which &lt;code&gt;playerId&lt;/code&gt; they belong to so future backend events can reach them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GameIdentityRegistry&lt;/code&gt; is the pattern that solves this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;A static lookup table. Two dictionaries. One clear API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GameIdentityRegistry&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_netIdToPlayerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_playerIdToNetId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_netIdToPlayerId&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_playerIdToNetId&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;NetworkIdentity&lt;/span&gt; &lt;span class="nf"&gt;GetNetworkObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&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="n"&gt;_playerIdToNetId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Check server first, then client&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NetworkServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spawned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;identity&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="n"&gt;NetworkClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spawned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetPlayerId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_netIdToPlayerId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_netIdToPlayerId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;_playerIdToNetId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&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;GetNetworkObject&lt;/code&gt; checks &lt;code&gt;NetworkServer.spawned&lt;/code&gt; before &lt;code&gt;NetworkClient.spawned&lt;/code&gt; — this ensures it works correctly in all Mirror roles: dedicated server, host, and client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registration — When and Where
&lt;/h2&gt;

&lt;p&gt;Registration happens in &lt;code&gt;PlayerIdentityBridge&lt;/code&gt;, a &lt;code&gt;NetworkBehaviour&lt;/code&gt; attached to the Mirror player prefab.&lt;/p&gt;

&lt;p&gt;The registration must happen on all instances — server/host and all clients — because &lt;code&gt;GetNetworkObject&lt;/code&gt; might be called on any of them when a backend event arrives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerIdentityBridge&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NetworkBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnStartLocalPlayer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FindObjectOfType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LobbyStateStore&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;CmdRegisterIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalPlayerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CmdRegisterIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Register on server/host&lt;/span&gt;
        &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netIdentity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Propagate to all clients&lt;/span&gt;
        &lt;span class="nf"&gt;RpcRegisterIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netIdentity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ClientRpc&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RpcRegisterIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;netId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&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;The flow: local player spawns → &lt;code&gt;OnStartLocalPlayer&lt;/code&gt; fires → &lt;code&gt;CmdRegisterIdentity&lt;/code&gt; runs on the server → &lt;code&gt;RpcRegisterIdentity&lt;/code&gt; propagates to all clients. Every instance now has the mapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage — Routing Backend Events
&lt;/h2&gt;

&lt;p&gt;When a Socket.IO event arrives with a &lt;code&gt;playerId&lt;/code&gt;, resolve it to a Mirror object and apply the effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In GameEventBridge.Subscribe()&lt;/span&gt;
&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;On&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"score_update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"playerId"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetNetworkObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerId&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="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&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="c1"&gt;// player may have left&lt;/span&gt;

    &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PlayerScore&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()?.&lt;/span&gt;&lt;span class="nf"&gt;SetScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;On&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"player_killed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;victimId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"victimId"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetNetworkObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;victimId&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="n"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&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="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PlayerHealth&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()?.&lt;/span&gt;&lt;span class="nf"&gt;Die&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;The null check on &lt;code&gt;GetNetworkObject&lt;/code&gt; is important — a player may have disconnected between when the server sent the event and when it arrived. Always guard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup — When and Where
&lt;/h2&gt;

&lt;p&gt;Clear the registry in exactly two places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. On ReturnToLobby — match ended normally&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ReturnToLobby&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... Mirror shutdown ...&lt;/span&gt;
    &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// ... LeaveRoom ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 2. On Socket.IO disconnect — unexpected disconnection&lt;/span&gt;
&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnDisconnected&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GameIdentityRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Missing either case leaves stale mappings. The next match starts with entries from the previous one, &lt;code&gt;GetNetworkObject&lt;/code&gt; returns wrong objects, and events apply to destroyed players. Subtle, intermittent, hard to reproduce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Static?
&lt;/h2&gt;

&lt;p&gt;A static class means no singleton MonoBehaviour, no inspector wiring, no &lt;code&gt;FindObjectOfType&lt;/code&gt;. Any component anywhere — &lt;code&gt;GameEventBridge&lt;/code&gt;, &lt;code&gt;PlayerIdentityBridge&lt;/code&gt;, a HUD script — can call &lt;code&gt;GameIdentityRegistry.Register()&lt;/code&gt; or &lt;code&gt;GameIdentityRegistry.GetNetworkObject()&lt;/code&gt; without a reference.&lt;/p&gt;

&lt;p&gt;This is safe because the registry's lifecycle is explicitly managed: &lt;code&gt;Register()&lt;/code&gt; populates it, &lt;code&gt;Clear()&lt;/code&gt; resets it. There's no implicit state — you always know exactly what's in it based on what's been registered since the last &lt;code&gt;Clear()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Broader Pattern
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GameIdentityRegistry&lt;/code&gt; is a specific instance of a general pattern: &lt;strong&gt;an identity bridge between two systems that use incompatible ID schemes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The same pattern applies anywhere two systems need to reference the same logical entity by different identifiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mirror &lt;code&gt;netId&lt;/code&gt; ↔ Steam &lt;code&gt;CSteamID&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Mirror &lt;code&gt;netId&lt;/code&gt; ↔ Photon &lt;code&gt;ActorNumber&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Mirror &lt;code&gt;netId&lt;/code&gt; ↔ any backend player ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implementation is always the same: two dictionaries, a &lt;code&gt;Register()&lt;/code&gt; call on spawn, a lookup on event receipt, a &lt;code&gt;Clear()&lt;/code&gt; on session end.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Context
&lt;/h2&gt;

&lt;p&gt;This pattern is part of the Mirror Integration sample in &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;socketio-unity&lt;/a&gt; — an open-source Socket.IO v4 client for Unity with full WebGL support.&lt;/p&gt;

&lt;p&gt;Install via Package Manager → Add package from git URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/Magithar/socketio-unity.git?path=/package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then import via Package Manager → Samples → "Mirror Integration" to see the full working implementation.&lt;/p&gt;

&lt;p&gt;MIT licensed. Zero paid dependencies.&lt;/p&gt;




&lt;p&gt;Have you needed to bridge IDs between two networking systems before? What was your approach?&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>multiplayer</category>
      <category>mirror</category>
    </item>
    <item>
      <title>10 Common Unity Rendering Issues (And How to Fix Them Like a Pro)</title>
      <dc:creator>Magithar Sridhar</dc:creator>
      <pubDate>Fri, 24 Apr 2026 18:07:02 +0000</pubDate>
      <link>https://dev.to/magithar/10-common-unity-rendering-issues-and-how-to-fix-them-like-a-pro-hcl</link>
      <guid>https://dev.to/magithar/10-common-unity-rendering-issues-and-how-to-fix-them-like-a-pro-hcl</guid>
      <description>&lt;p&gt;If you spend enough time building in Unity, you start to see the same visual bugs over and over again. Weird shadows, flickering textures, scenes that look washed out for no apparent reason — we've all been there.&lt;/p&gt;

&lt;p&gt;When you're trying to ship a game, spending hours debugging a shadow artifact is the last thing you want to do.&lt;/p&gt;

&lt;p&gt;Over time I've built up a mental cheat sheet for diagnosing these issues quickly. Here are the 10 most common ones, what causes them, and the fastest way to fix each.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Shadow Artifacts (Stripes or "Acne")
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Shadow bias or precision issues. The object is casting a shadow on itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Increase the &lt;strong&gt;Shadow Bias&lt;/strong&gt; and &lt;strong&gt;Normal Bias&lt;/strong&gt; on your light source.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you push the bias too high, the shadow will detach from the object (known as "Peter Panning"). Dial it back just enough until the acne disappears.&lt;/p&gt;
&lt;/blockquote&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.amazonaws.com%2Fuploads%2Farticles%2Fw7vixezerr1opve261dq.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.amazonaws.com%2Fuploads%2Farticles%2Fw7vixezerr1opve261dq.png" alt="Shadow artifacts showing striped shadow acne on the sloped face of a white geometric object caused by rendering issues" width="400" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Jagged or Pixelated Shadows
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Low shadow resolution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Go to Quality Settings and increase &lt;strong&gt;Shadow Resolution&lt;/strong&gt;. If you're using a Directional Light, tweaking &lt;strong&gt;Shadow Cascades&lt;/strong&gt; can dramatically improve quality near the camera.&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.amazonaws.com%2Fuploads%2Farticles%2Fe697qvul69l8svt6jh29.jpg" 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.amazonaws.com%2Fuploads%2Farticles%2Fe697qvul69l8svt6jh29.jpg" alt="Jagged pixelated shadows with visible stair-step edges cast beside a circular tower structure in a 3D scene" width="406" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Light Bleeding Through Walls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Light leaking — usually caused by thin meshes or bias settings that are too high.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Check shadow bias and normal bias first. If those are fine, ensure your meshes have actual thickness. Single-sided planes are notorious for light leaks.&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.amazonaws.com%2Fuploads%2Farticles%2Fmy2i7zm1yzkm3quhi1zs.jpeg" 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.amazonaws.com%2Fuploads%2Farticles%2Fmy2i7zm1yzkm3quhi1zs.jpeg" alt="Light bleeding through walls with bright sunlight leaking into the corners and floor of an indoor room in a 3D scene" width="690" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Objects Look Too Dark or Flat
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Missing Global Illumination or an incomplete lighting setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Ensure you have ambient light or skybox lighting active. If you're using static geometry, baking your lighting usually solves this and adds much-needed depth.&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.amazonaws.com%2Fuploads%2Farticles%2Fmp0n0cd3wdc5i4wqzv3y.jpeg" 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.amazonaws.com%2Fuploads%2Farticles%2Fmp0n0cd3wdc5i4wqzv3y.jpeg" alt="3D forest game scene where the player character and objects appear overly dark and flat due to poor lighting or missing shadows" width="690" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Overbright or Washed Out Scene
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Exposure or Post-Processing Tonemapping issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Check your Post-Processing Volume — specifically &lt;strong&gt;Auto Exposure&lt;/strong&gt; and &lt;strong&gt;Color Grading/Tonemapping&lt;/strong&gt;. Default auto-exposure can go wild in high-contrast scenes.&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.amazonaws.com%2Fuploads%2Farticles%2Fvb6zq8cew9qi09k79wd7.jpeg" 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.amazonaws.com%2Fuploads%2Farticles%2Fvb6zq8cew9qi09k79wd7.jpeg" alt="Unity editor scene with extreme overexposure where the indoor environment appears overbright and washed out, causing loss of detail" width="800" height="616"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Flickering Textures (Z-Fighting)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Depth precision issues where two faces occupy the exact same space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Make sure your meshes aren't perfectly overlapping. If they aren't, adjust the &lt;strong&gt;Near&lt;/strong&gt; and &lt;strong&gt;Far Clip Planes&lt;/strong&gt; on your Camera. Moving the Near Clip Plane further away increases depth precision.&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.amazonaws.com%2Fuploads%2Farticles%2Fwlbbfkyjvvxqhc465lht.jpeg" 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.amazonaws.com%2Fuploads%2Farticles%2Fwlbbfkyjvvxqhc465lht.jpeg" alt="3D object showing flickering texture artifacts caused by z-fighting where overlapping surfaces compete to render in the same space" width="227" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Blurry Textures at an Angle
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Texture import settings — specifically filtering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Select the texture in your Project window and increase the &lt;strong&gt;Anisotropic Filtering&lt;/strong&gt; level. This keeps textures sharp when viewed at oblique angles like roads or floors.&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.amazonaws.com%2Fuploads%2Farticles%2Ftkm7jtdr007ltvtbve7k.jpeg" 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.amazonaws.com%2Fuploads%2Farticles%2Ftkm7jtdr007ltvtbve7k.jpeg" alt="Road texture becoming blurry at an angle in a 3D scene, highlighted to show loss of detail in the distance" width="318" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Sudden Performance Drops in Complex Scenes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; High overdraw (drawing transparent pixels on top of each other) or too many objects pushing the GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Implement LODs (Level of Detail), use GPU Instancing for repeated objects, and check your overdraw in the Scene window's draw mode selector.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Too Many Draw Calls (CPU Bottleneck)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Unbatched objects. The CPU is telling the GPU to draw too many individual things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Enable &lt;strong&gt;Static Batching&lt;/strong&gt; for non-moving objects. For dynamic objects sharing the same material, ensure &lt;strong&gt;GPU Instancing&lt;/strong&gt; or &lt;strong&gt;Dynamic Batching&lt;/strong&gt; is enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Weird Lighting or Dark Patches on Meshes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The cause:&lt;/strong&gt; Broken or inverted normals on the 3D model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Select the model in your Project window, go to Import Settings, and set Normals to &lt;strong&gt;Calculate&lt;/strong&gt; instead of Import. If that doesn't work, recalculate normals in Blender or Maya.&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.amazonaws.com%2Fuploads%2Farticles%2Fordysgi86nm6av7yjnf3.jpg" 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.amazonaws.com%2Fuploads%2Farticles%2Fordysgi86nm6av7yjnf3.jpg" alt="3D mesh surface showing weird lighting artifacts with uneven bright areas and dark patches caused by incorrect normals or lighting issues" width="800" height="542"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;First thing to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shadow stripes / acne&lt;/td&gt;
&lt;td&gt;Self-shadowing&lt;/td&gt;
&lt;td&gt;Shadow Bias on light&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jagged shadows&lt;/td&gt;
&lt;td&gt;Low resolution&lt;/td&gt;
&lt;td&gt;Quality Settings → Shadow Resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Light through walls&lt;/td&gt;
&lt;td&gt;Thin mesh / high bias&lt;/td&gt;
&lt;td&gt;Mesh thickness + bias values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flat / dark objects&lt;/td&gt;
&lt;td&gt;Missing GI&lt;/td&gt;
&lt;td&gt;Ambient light or baked lighting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Washed out scene&lt;/td&gt;
&lt;td&gt;Post-processing&lt;/td&gt;
&lt;td&gt;Auto Exposure / Tonemapping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flickering textures&lt;/td&gt;
&lt;td&gt;Z-fighting&lt;/td&gt;
&lt;td&gt;Near/Far Clip Planes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blurry angled textures&lt;/td&gt;
&lt;td&gt;Filtering&lt;/td&gt;
&lt;td&gt;Anisotropic Filtering level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance drops&lt;/td&gt;
&lt;td&gt;Overdraw / GPU load&lt;/td&gt;
&lt;td&gt;LOD + overdraw view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Too many draw calls&lt;/td&gt;
&lt;td&gt;Unbatched objects&lt;/td&gt;
&lt;td&gt;Static/Dynamic Batching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dark patches on mesh&lt;/td&gt;
&lt;td&gt;Bad normals&lt;/td&gt;
&lt;td&gt;Import Settings → Normals: Calculate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;I ran into almost all of these while building out my recent multiplayer projects. Once you learn to recognize the symptoms, fixing them becomes second nature.&lt;/p&gt;

&lt;p&gt;What rendering issue trips you up most? Got a bug that's driving you crazy right now? Drop a screenshot in the comments and I'll do my best to help diagnose it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Currently building a Unity WebGL multiplayer library in public — &lt;a href="https://github.com/Magithar/socketio-unity" rel="noopener noreferrer"&gt;socketio-unity&lt;/a&gt;. Follow along to see more of the technical hurdles I run into.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>tutorial</category>
      <category>graphics</category>
    </item>
  </channel>
</rss>
