<?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: Artemr Rudenko</title>
    <description>The latest articles on DEV Community by Artemr Rudenko (@artemr_rudenko_0bf2c2c505).</description>
    <link>https://dev.to/artemr_rudenko_0bf2c2c505</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%2F1730126%2Fcc09aa7f-ea7e-4f38-bbcc-532d9449fd9e.jpg</url>
      <title>DEV Community: Artemr Rudenko</title>
      <link>https://dev.to/artemr_rudenko_0bf2c2c505</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/artemr_rudenko_0bf2c2c505"/>
    <language>en</language>
    <item>
      <title>I built a governance layer for AI agent skills — with an AI agent as my pair programmer</title>
      <dc:creator>Artemr Rudenko</dc:creator>
      <pubDate>Mon, 03 Aug 2026 16:16:50 +0000</pubDate>
      <link>https://dev.to/artemr_rudenko_0bf2c2c505/i-built-a-governance-layer-for-ai-agent-skills-with-an-ai-agent-as-my-pair-programmer-4jkb</link>
      <guid>https://dev.to/artemr_rudenko_0bf2c2c505/i-built-a-governance-layer-for-ai-agent-skills-with-an-ai-agent-as-my-pair-programmer-4jkb</guid>
      <description>&lt;h2&gt;
  
  
  The moment I noticed the problem
&lt;/h2&gt;

&lt;p&gt;Somewhere around late May 2026, I had a folder full of &lt;code&gt;SKILL.md&lt;/code&gt; files. Some were mine, some were pulled from a corporate library, some were written half-tired at 11pm to unblock a task. They all &lt;em&gt;worked&lt;/em&gt;, in the sense that the agent picked them up and did something. But I couldn't have told you, with a straight face, which ones were safe to hand to a teammate, which ones would silently break the moment someone tried them in Cursor instead of Claude Code, and which ones had a &lt;code&gt;curl | bash&lt;/code&gt; sitting quietly in a code block that nobody had ever read past the first two lines.&lt;/p&gt;

&lt;p&gt;That's the actual origin story. Not a grand vision — a folder of skills I didn't trust.&lt;/p&gt;

&lt;p&gt;Two open standards had recently landed, within months of each other: &lt;strong&gt;AGENTS.md&lt;/strong&gt; (the Linux Foundation AAIF format for repo-wide agent instructions) and &lt;strong&gt;SKILL.md&lt;/strong&gt; (Anthropic's Agent Skills spec for packaged, on-demand procedures). Both are refreshingly simple — frontmatter plus Markdown, no build step, no SDK lock-in. That simplicity is exactly why they were spreading fast, and exactly why nobody had built the boring, unglamorous layer every fast-spreading format eventually needs: &lt;strong&gt;governance&lt;/strong&gt;. Something that scores a skill, audits a library of them, checks whether a "portable" skill is actually portable, and catches the security issues a human reviewer skims past on the fifth PR of the day.&lt;/p&gt;

&lt;p&gt;So I built one. Six meta-skills, a security-focused sub-agent, and a deterministic audit engine — all designed to run identically on Claude Code, GitHub Copilot, Cursor, OpenAI Codex CLI, and Google Gemini CLI, because a governance tool that only works on one vendor's runtime isn't governance, it's a walled garden.&lt;/p&gt;

&lt;p&gt;I did not write this alone. Claude Code wrote most of the actual code and Markdown; I made the calls on architecture, scope, and what "good enough" meant at each stage. That division of labor is worth being honest about, because it's part of &lt;em&gt;how&lt;/em&gt; this got built as fast as it did — more on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually shipped
&lt;/h2&gt;

&lt;p&gt;The repo is called &lt;code&gt;skill-governance-toolkit&lt;/code&gt;, and it's a "reference implementation of the universal skill authoring pattern" — meaning it doesn't just describe the AGENTS.md + SKILL.md standard, it enforces it against itself. The toolkit's own skills pass its own audit. If they didn't, that would be the whole pitch falling over.&lt;/p&gt;

&lt;p&gt;Six meta-skills, all operating on &lt;em&gt;other&lt;/em&gt; skills rather than on application code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;You'd say&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;skill-find&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search project/personal/corporate skill libraries for something matching a need&lt;/td&gt;
&lt;td&gt;"is there a skill for X"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;skill-evaluate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Score one &lt;code&gt;SKILL.md&lt;/code&gt; across 9 quality dimensions (D1–D9) + a lexical security scan&lt;/td&gt;
&lt;td&gt;"is this skill production-ready"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;skill-compare&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Diff a local skill against a corporate version, recommend USE / KEEP / MERGE&lt;/td&gt;
&lt;td&gt;"is there a similar one upstream"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;library-audit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Meta-validate a whole library — handoff contracts, token budget, naming, security baseline&lt;/td&gt;
&lt;td&gt;"audit my skills"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;skill-build-portable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turn a Claude-only skill into a cross-runtime one, diff preview before write&lt;/td&gt;
&lt;td&gt;"make this skill portable"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;integration-init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bootstrap optional tooling (code-review-graph, repomix, serena) per project&lt;/td&gt;
&lt;td&gt;"set up integrations"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus &lt;code&gt;agents/code-reviewer.md&lt;/code&gt;, a TypeScript-focused sub-agent with five review categories, dispatchable from any orchestrator skill.&lt;/p&gt;

&lt;p&gt;And under all of it, a deterministic audit engine (&lt;code&gt;agent_audit.py&lt;/code&gt;) that ships in three delivery modes — CLI for local checks and CI gates, MCP (Anthropic's Model Context Protocol, the standard agents use to call external tools) so an agent can call it mid-edit as a tool, and a sandboxed Docker image for teams that don't want local Python at all. Same engine, same rules, three ways to invoke it.&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%2Fh1tuir1d75djsonedaxx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh1tuir1d75djsonedaxx.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it got built — and how fast
&lt;/h2&gt;

&lt;p&gt;Here's a detail I find more interesting than the feature list: the CHANGELOG we maintain ourselves — the project's own dated release record — shows &lt;strong&gt;42 versioned releases in 18 days&lt;/strong&gt;, v1.0.0 on May 23 through v1.20.0 on June 10, 2026. It wasn't an even drip either: 24 of those releases landed in the first three days, another 13 landed in a single day about two weeks later, with quieter single-release days connecting the two bursts. That kind of pace only works with a strict discipline underneath it, and the discipline was non-negotiable: &lt;strong&gt;red first, then green, every time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Concretely: every change gets a failing test written before the fix, then the fix, then the &lt;em&gt;entire&lt;/em&gt; self-CI suite has to pass before anything gets committed. Right now that suite is 37 phases deep (the project literally numbers them P1 through P37) — covering everything from "does the CLI behave correctly" to "does the Windows installer's PowerShell script parse" to "does the documented phase range in AGENTS.md match what audit.sh actually runs" (yes, there's a phase whose entire job is catching &lt;em&gt;documentation drift about the test suite itself&lt;/em&gt; — P35, if you're curious).&lt;/p&gt;

&lt;p&gt;The architecture decision I'm proudest of isn't a feature, it's a &lt;strong&gt;fork in the road that the toolkit makes explicit for anyone who wants to add a new check.&lt;/strong&gt; There are two surfaces, and picking the wrong one is the most common mistake:&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%2Ftbr6cavqrmm9hmz6xmff.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftbr6cavqrmm9hmz6xmff.png" alt=" " width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Surface A&lt;/strong&gt; is the deterministic gate — regex and structural rules, running in CI, blocking, zero tolerance for false positives. &lt;strong&gt;Surface B&lt;/strong&gt; is the LLM-scored skill layer — &lt;code&gt;skill-evaluate&lt;/code&gt;'s rubric, &lt;code&gt;library-audit&lt;/code&gt;'s cross-skill checks — where judging intent actually requires reading and understanding, not pattern-matching. The rule of thumb the docs state plainly: &lt;em&gt;a semantic check forced into the gate becomes a false-positive machine; a security invariant left to the LLM is not a gate at all.&lt;/em&gt; Getting that boundary right early saved a lot of wasted rework later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually checks for
&lt;/h2&gt;

&lt;p&gt;The deterministic gate currently carries 17 distinct rule IDs across five families:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SEC001&lt;/code&gt;–&lt;code&gt;SEC003&lt;/code&gt;&lt;/strong&gt; — bidirectional-text overrides, invisible/zero-width characters, homoglyph typosquats (&lt;code&gt;pаypal.com&lt;/code&gt; with a Cyrillic а). The kind of thing a human reviewer will never catch by eye.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SEC010&lt;/code&gt;–&lt;code&gt;SEC012&lt;/code&gt;&lt;/strong&gt; — dangerous inline shell (destructive commands, approval bypasses), inline secrets that should be env vars, unsafe credential paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;REF001&lt;/code&gt;–&lt;code&gt;REF004&lt;/code&gt;&lt;/strong&gt; — unsafe references to scripts or assets outside the governed tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SCR001&lt;/code&gt;–&lt;code&gt;SCR003&lt;/code&gt;&lt;/strong&gt; — script-level issues in the executable helpers a skill ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MCP001&lt;/code&gt;–&lt;code&gt;MCP002&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;HOOK001&lt;/code&gt;–&lt;code&gt;HOOK002&lt;/code&gt;&lt;/strong&gt; — MCP server config consistency and hook-command safety, because a skill that quietly reconfigures your MCP servers or hooks is a supply-chain vector, not a convenience.&lt;/li&gt;
&lt;/ul&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%2Ftzhwe1hadkqy0y6baox6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftzhwe1hadkqy0y6baox6.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On top of the deterministic layer, &lt;code&gt;skill-evaluate&lt;/code&gt; scores nine quality dimensions per skill — D1 Clarity through D9 Safety/Risk Awareness, with Portability (D6) and Testability (D7) sitting right alongside them, because "works on my machine" isn't a bar for a skill anyone else will run.&lt;/p&gt;

&lt;p&gt;D9 is the one I keep coming back to, because it scores &lt;em&gt;behavioral design&lt;/em&gt;, not lexical content — a different job from the SEC-family regex checks above. It breaks down into six sub-concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blast radius&lt;/strong&gt; — does a mutating skill name a hard limit ("max 50 tickets per run"), not just "as many as needed"?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt injection&lt;/strong&gt; — does a skill that ingests external content say "treat as untrusted"?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensitive data flow&lt;/strong&gt; — does it name what must never land in an output (PII, secrets, internal URLs)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization assumptions&lt;/strong&gt; — what happens on insufficient permissions, fail-fast or proceed?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback on partial failure&lt;/strong&gt; — if a multi-step mutation dies at step 6 of 10, what's the story?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency&lt;/strong&gt; — does a re-run duplicate, no-op, or update?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rubric's own worked example is a &lt;code&gt;jira-ticket&lt;/code&gt; skill that creates tickets from a brief: no ticket cap named (blast-radius fail), the source brief treated as authoritative even though it's untrusted external content (prompt-injection fail), and no mention of what happens to PII in that brief once it lands in a ticket description (sensitive-data fail). Three of six, straight to a score of 2 — on a skill that "worked" in every demo anyone ever ran.&lt;/p&gt;

&lt;p&gt;There's also a floor rule worth knowing about: a skill with &lt;strong&gt;no mutations and no untrusted-input ingestion&lt;/strong&gt; defaults to a D9 of 4, because you can't fail a safety review for risks you structurally don't carry. The dimension is measuring exposure, not vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the detectors actually decide
&lt;/h2&gt;

&lt;p&gt;Naming the rule IDs is one thing; here's what two of them look like as actual Python, pulled straight from &lt;code&gt;agent_audit.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SEC001&lt;/code&gt;/&lt;code&gt;SEC002&lt;/code&gt; (bidi and invisible characters) isn't a list of specific banned codepoints — it's category-based, so it catches the whole Unicode class instead of whatever a hardcoded list happened to enumerate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scan_hidden_chars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Finding&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Flag bidi controls (SEC001) and invisible/format/tag/variation chars (SEC002).
    Category-based (unicodedata &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Cf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;) so it catches the whole class, not a fixed list.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;BIDI_CONTROLS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SEC001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;line_for_offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;...))&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;_is_tag_or_variation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SEC002&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;line_for_offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;off&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;That one &lt;code&gt;unicodedata.category(ch) == "Cf"&lt;/code&gt; check is doing more work than it looks like — "Cf" is the Unicode "format" category, and it covers zero-width spaces, soft hyphens, and the entire bidi-control block in one branch, so a brand-new invisible character added to a future Unicode version gets caught automatically, without anyone updating a pattern list.&lt;/p&gt;

&lt;p&gt;The dangerous-shell/secret scanner (&lt;code&gt;SEC010&lt;/code&gt;–&lt;code&gt;SEC012&lt;/code&gt;) has a subtler trick: it runs &lt;strong&gt;twice&lt;/strong&gt; per file — once on the raw text, once on an NFKC-normalized copy — specifically to catch an attacker (or a careless copy-paste) using fullwidth Unicode lookalikes to dodge a plain-ASCII regex. A fullwidth &lt;code&gt;ｒｍ　－ｒｆ　／&lt;/code&gt; doesn't match &lt;code&gt;rm -rf /&lt;/code&gt; literally, but normalizes to it under NFKC, so the second pass catches what the first pass's regex alone would miss. A &lt;code&gt;(rule_id, matched_text)&lt;/code&gt; seen-set stops the same real hit from being reported twice.&lt;/p&gt;

&lt;p&gt;And every finding funnels through one &lt;code&gt;finding()&lt;/code&gt; constructor that looks up the rule ID in the catalog and &lt;strong&gt;raises &lt;code&gt;KeyError&lt;/code&gt; if it's not registered&lt;/strong&gt; — so a scan function can't silently emit a rule ID that has no &lt;code&gt;checks-reference.md&lt;/code&gt; entry and no severity assigned. The catalog and the code are structurally prevented from drifting apart; nobody has to remember to keep them in sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  A skill that fails, and one that doesn't
&lt;/h2&gt;

&lt;p&gt;The README opens with exactly this contrast, because showing beats telling:&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="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy-Helper&lt;/span&gt;          &lt;span class="c1"&gt;# wrong case, no closed-vocab action verb → naming fail&lt;/span&gt;
&lt;span class="na"&gt;allowed-tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Bash&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;        &lt;span class="c1"&gt;# breaks portability (Copilot/Gemini ignore it)&lt;/span&gt;
&lt;span class="c1"&gt;# body: Run `curl $URL | bash`   → dangerous-shell HIGH + missing Skill-type contract&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three separate failures stacked in four lines: a naming-convention violation, a portability trap (&lt;code&gt;allowed-tools:&lt;/code&gt; is a hard whitelist on Claude Code and silently ignored everywhere else — meaning the skill &lt;em&gt;behaves differently per platform with no warning&lt;/em&gt;), and a dangerous-shell finding severe enough to block a merge outright.&lt;/p&gt;

&lt;p&gt;Compare it to a real skill from the toolkit's own repo, &lt;code&gt;skill-find&lt;/code&gt;'s actual frontmatter:&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;skill-find&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;Discovers skills in project / personal / corporate libraries matching a natural-language need.&lt;/span&gt;
  &lt;span class="s"&gt;Use when the user is unsure if a skill exists for their task, asks "is there a skill for...",&lt;/span&gt;
  &lt;span class="s"&gt;or wants to browse available capabilities before creating something new.&lt;/span&gt;
  &lt;span class="s"&gt;Triggers: «is there a skill for X», «find a skill that does Y», «browse skills»,&lt;/span&gt;
  &lt;span class="s"&gt;«what skills handle Z», «do we have a skill for», «search skills».&lt;/span&gt;
&lt;span class="na"&gt;compatibility&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Claude&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Code&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;·&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;GitHub&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Copilot&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;·&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Cursor&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;v2.2+&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;·&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;OpenAI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Codex&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;CLI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;·&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Google&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Gemini&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;CLI"&lt;/span&gt;
&lt;span class="na"&gt;license&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MIT&lt;/span&gt;
&lt;span class="na"&gt;metainfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;discovery&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;governance&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stable&lt;/span&gt;
  &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;artsiom&lt;/span&gt;
  &lt;span class="na"&gt;added&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-05-23"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;allowed-tools:&lt;/code&gt;. An explicit &lt;code&gt;compatibility:&lt;/code&gt; line naming every runtime it's tested against. A description that answers both "what does this do" and "when should you trigger it," with real trigger phrases in the user's own words — because &lt;code&gt;skill-evaluate&lt;/code&gt;'s D8 (Discoverability) dimension scores exactly that, and a skill nobody's agent ever activates is dead weight regardless of how good its logic is.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;skill-build-portable&lt;/code&gt; automates the gap between those two examples. Feed it a Claude-only skill with &lt;code&gt;allowed-tools&lt;/code&gt;, hardcoded paths, and a 16-character description, and it produces a diff, not a lecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transformations applied (5):
  🔴 F1 · frontmatter:5 · remove allowed-tools (non-portable hard whitelist)
  🟡 F2 · frontmatter:3 · remove type:, status: (not in agentskills.io spec)
  🟡 F3 · frontmatter:8 · remove paths: (Claude-only; document in body if path-scope matters)
  🔴 B1 · body:line 1 · replace _audits/internal/ → out/audits/internal/
  🟡 F7 · add compatibility: field

Description too short (16 chars). Required ≥ 80 to score well on D8.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it again on an already-portable skill and the honest answer is zero changes — the tool proves its own idempotency instead of asserting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending it without forking it
&lt;/h2&gt;

&lt;p&gt;The thing I wanted to avoid most was becoming the toolkit's single point of contact for every new rule. So the two-surface split from earlier doubles as the contribution contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New pattern for an existing detector&lt;/strong&gt; (a new dangerous command, a new secret shape) — that's policy &lt;em&gt;data&lt;/em&gt;, not code. It goes into a config overlay, no Python touched:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"patterns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dangerous"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kubectl delete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"regex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kubectl&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;s+delete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"suggestion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No cluster deletion in assets."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before it ships, a ReDoS budget guard — ReDoS being a regex pattern that pathologically backtracks and hangs on certain input — rejects catastrophic patterns automatically, so a badly written rule can't take down CI by freezing it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A genuinely new kind of detection&lt;/strong&gt; — that's a code change, and the bar is explicit: write the &lt;code&gt;scan_*&lt;/code&gt; function, register the rule ID, add a severity, document it, and — this is the part I like — &lt;strong&gt;ship a triggering fixture &lt;em&gt;and&lt;/em&gt; a clean near-miss fixture.&lt;/strong&gt; A rule that only has a positive example can't prove it isn't over-broad. No clean fixture, no merge.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Semantic judgment that can't be a regex&lt;/strong&gt; — new D-dimension sub-check in &lt;code&gt;skill-evaluate&lt;/code&gt;, or a new phase in &lt;code&gt;library-audit&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That contract is also why the self-CI suite reached 37 phases without becoming unmaintainable: every new capability came with the fixtures that keep it honest, checked in alongside it, not bolted on after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more pattern worth stealing: checkpointed runs
&lt;/h2&gt;

&lt;p&gt;Multi-phase skills — &lt;code&gt;library-audit&lt;/code&gt; walking a hundred-skill repo, &lt;code&gt;skill-evaluate&lt;/code&gt; scoring through nine dimensions plus a security pass — take real time, and real time means real interruptions: a timeout, a crash, someone hitting Ctrl-C because a meeting started. The toolkit's answer is a small, boring, load-bearing convention: after every expensive phase, write state to &lt;code&gt;${CWD}/.skill-state/{skill-name}/run-{utc-ts}.json&lt;/code&gt; in the &lt;em&gt;project being audited&lt;/em&gt;, not the toolkit checkout, so one toolkit install can safely service many projects without their state colliding. An interrupted &lt;code&gt;library-audit&lt;/code&gt; run picks up from the last completed phase instead of re-scoring a hundred skills from zero. It's not a clever feature. It's the kind of unglamorous reliability work that only gets built when someone's actually been burned by its absence — which, if I'm honest, is most of what's in this toolkit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What running it actually looks like
&lt;/h2&gt;

&lt;p&gt;Not a mockup — this is the real self-audit gate, one phase of 37, testing that a blocking finding actually blocks and that the kill-switch actually downgrades it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;── P22 — Delta-gate integration (real git: --fail-on-new + kill-switch) ──
  ✓ --fail-on-new high blocks on the newly introduced finding (exit 1)
  ✓ SGT_FAIL_MODE=advisory downgrades the gate to non-blocking (exit 0)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the tail of a full run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;══════════════════════════════════════════════════════════
Audit summary:
  ✓ passed:   determined by zero failures
  ! warned:   1
  ✗ failed:   0
══════════════════════════════════════════════════════════
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(That one warning, every time, is "pwsh not on PATH" — expected on a Mac without PowerShell installed, not a real failure.)&lt;/p&gt;

&lt;p&gt;And here's the "three delivery modes, one engine" claim made concrete — the Docker smoke test literally pipes a raw MCP JSON-RPC call into the sandboxed image and expects a real answer back, no local Python involved:&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;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"audit_project","arguments":{"root":"/workspace","profile":"release-ready"}}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--network&lt;/span&gt; none &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;:/workspace:ro"&lt;/span&gt; agent-governance-mcp:ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--network none&lt;/code&gt; isn't incidental — the audit engine is read-only and offline by design, so the smoke test proves it doesn't &lt;em&gt;need&lt;/em&gt; a network to do its job, not just that it happens not to use one.&lt;/p&gt;

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

&lt;p&gt;The toolkit is public now — MIT licensed, GitHub Actions CI green (self-audit, Windows-parity, Docker smoke test), and its own release notes are generated from &lt;code&gt;CHANGELOG.md&lt;/code&gt; by the same script that gates its own merges, so there's no hand-maintained notes file to drift out of sync.&lt;/p&gt;

&lt;p&gt;If you're standing in the same spot I was in May — a folder of skills you can't quite vouch for — the six-skill tour starts with &lt;code&gt;skill-evaluate&lt;/code&gt; on whatever &lt;code&gt;SKILL.md&lt;/code&gt; you're least sure about. It'll tell you, with a score and a reason, whether "least sure about" was the right instinct.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/artemrudenko/skill-governance-toolkit" rel="noopener noreferrer"&gt;github.com/artemrudenko/skill-governance-toolkit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>devtools</category>
      <category>security</category>
    </item>
  </channel>
</rss>
