<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>skillfold blog</title>
    <link>https://byronxlg.com/skillfold/blog/</link>
    <description>On agent configuration, skill distribution, and the supply chain underneath them: what is happening in the ecosystem and what it means in practice.</description>
    <language>en</language>
    <lastBuildDate>Sat, 26 Sep 2026 12:00:00 GMT</lastBuildDate>
    <atom:link href="https://byronxlg.com/skillfold/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>A CLI&#39;s skill can now follow the version of the CLI you installed</title>
      <link>https://byronxlg.com/skillfold/blog/installed-skills/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/installed-skills/</guid>
      <pubDate>Sat, 26 Sep 2026 12:00:00 GMT</pubDate>
      <description>Skillfold&#39;s @installed ref pins a skill shipped inside an npm package to the version your project has installed, and fails the check when they drift.</description>
      <content:encoded><![CDATA[<p>More command-line tools now ship an agent skill inside their own npm
package. <code>@playwright/cli</code> carries one at <code>skills/playwright-cli</code>.
<code>hyperframes</code> carries three under <code>dist/skills</code>. skillfold ships
<code>skillfold-cli</code> in its <code>agentskills</code> map. The idea is sound: the people who
write the tool write the instructions for driving it, and the instructions
travel with the code.</p>
<p>It also creates a quiet version problem. A skill bundled with a tool
describes that exact release of the tool. Your project pins the tool in
<code>package-lock.json</code> and the skill in <code>skillfold.lock</code>, and nothing ties the
two numbers together. When Dependabot bumps the dependency, the tool moves
and the skill stays behind.</p>
<p>How far behind matters. <code>@playwright/cli</code> 0.1.18 was published to npm on
August 6, 2026 and 0.1.21 on September 18, per the registry&#39;s own
timestamps. Between those versions the bundled <code>SKILL.md</code> gained an
emulation section (<code>set-color-scheme</code>, <code>set-reduced-motion</code>, and friends),
<code>recording-start</code> and <code>recording-stop</code>, a WebMCP section with
<code>webmcp-call</code>, and a new <code>references/pr-attachments.md</code> file. An agent
reading the 0.1.18 skill against a 0.1.21 binary never learns those
commands exist. Nothing errors; the agent knows less than the tool can
do.</p>
<p>The reverse is worse. Roll the dependency back and the agent is now
reading documentation for flags the installed binary does not have.</p>
<h2 id="one-ref-installed">One ref: <code>@installed</code></h2>
<p>Skillfold, which manages agent skills from a manifest and a lockfile, now
accepts <code>@installed</code> as the version of an npm source:</p>
<pre><code class="language-yaml">skills:
  playwright-cli: npm:@playwright/cli/skills/playwright-cli@installed
  hyperframes-cli: npm:hyperframes/dist/skills/hyperframes-cli@installed
  skillfold: npm:skillfold/skillfold-cli@installed
</code></pre>
<p><code>@installed</code> means &quot;whatever version of this package the project has
installed&quot;. Skillfold reads that version offline, without the registry:</p>
<ul>
<li>the nearest <code>npm-shrinkwrap.json</code>, <code>package-lock.json</code>, or
<code>pnpm-lock.yaml</code>, walking up to the repository root so workspace packages
find the root lockfile;</li>
<li>then <code>node_modules</code>, through Node&#39;s own resolution, which covers yarn and
bun installs.</li>
</ul>
<p>The lockfile is read before <code>node_modules</code>, so a fresh clone resolves
before anyone has run <code>npm ci</code>, and a stale <code>node_modules</code> does not win
over what the lockfile says.</p>
<h2 id="what-it-looks-like">What it looks like</h2>
<p>This is a scratch project with <code>@playwright/cli@0.1.18</code> and
<code>skillfold@2.6.0</code> installed as dev dependencies, run with a build of
skillfold&#39;s <code>main</code> branch:</p>
<pre><code class="language-console">$ skillfold install
  + playwright-cli           npm:@playwright/cli/skills/playwright-cli@installed -&gt; 0.1.18
  + skillfold                npm:skillfold/skillfold-cli@installed -&gt; 2.6.0

2 installed, 0 unchanged -&gt; .claude/skills
lockfile: skillfold.lock
</code></pre>
<p>The lockfile format does not change. The source keeps <code>@installed</code>, and
the resolved pin is still an exact version with a content hash:</p>
<pre><code class="language-yaml">  playwright-cli:
    source: npm:@playwright/cli/skills/playwright-cli@installed
    resolved: npm:@playwright/cli/skills/playwright-cli@0.1.18
</code></pre>
<p>Now bump both dependencies the way Dependabot would, and touch nothing
else:</p>
<pre><code class="language-console">$ npm install -D @playwright/cli@0.1.21 skillfold@2.7.0
$ skillfold list
  name            source                                               pinned  status
  playwright-cli  npm:@playwright/cli/skills/playwright-cli@installed  0.1.18  stale
  skillfold       npm:skillfold/skillfold-cli@installed                2.6.0   stale
</code></pre>
<p><code>skillfold check</code>, the offline command CI runs, fails and says why:</p>
<pre><code class="language-console">$ skillfold check
skillfold check failed:
  - &quot;playwright-cli&quot; follows @playwright/cli@installed: 0.1.21 is installed (package-lock.json) but the lockfile pins 0.1.18 (run &quot;skillfold install&quot;)
  - &quot;skillfold&quot; follows skillfold@installed: 2.7.0 is installed (package-lock.json) but the lockfile pins 2.6.0 (run &quot;skillfold install&quot;)
</code></pre>
<p><code>skillfold install --frozen</code> refuses for the same reason. A dependency PR
that forgot to refresh the skills therefore fails its own checks, instead
of merging a skill that no longer matches the tool.</p>
<p>The fix is the ordinary command:</p>
<pre><code class="language-console">$ skillfold install
  + playwright-cli           npm:@playwright/cli/skills/playwright-cli@installed -&gt; 0.1.21
  + skillfold                npm:skillfold/skillfold-cli@installed -&gt; 2.7.0

2 installed, 0 unchanged -&gt; .claude/skills
lockfile: skillfold.lock
$ skillfold check
ok: 2 skills in sync
</code></pre>
<p>This is a deliberate exception to one of skillfold&#39;s rules. Normally
<code>install</code> never moves an existing pin and only <code>skillfold update</code> does. A
skill pinned with <code>@installed</code> has already said what it wants to track,
so <code>install</code> re-pins it whenever the dependency moved, in either
direction. Every other source keeps the old behavior.</p>
<h2 id="global-mode-and-init">Global mode and <code>init</code></h2>
<p>User-level skills (<code>skillfold install -g</code>) have no project and no
lockfile, so there <code>@installed</code> follows the globally installed package,
the directory <code>npm root -g</code> reports, and reads the skill from that
directory without downloading anything. For skillfold&#39;s own skill it
falls back to the running CLI when skillfold is not installed globally.
<code>install -g</code> and <code>check -g</code> also warn when any skill from the skillfold
package is pinned to a different version than the CLI running the
command.</p>
<p><code>skillfold init</code> uses the new ref where it can. When skillfold is a
dependency of the project, or with <code>-g</code>, the scaffolded manifest declares
<code>skillfold: npm:skillfold/skillfold-cli@installed</code> rather than an
unpinned source that resolves to whatever was latest on the day you ran
<code>init</code>.</p>
<h2 id="migrating">Migrating</h2>
<p>Nothing breaks. Existing manifests, lockfiles, and pins keep working
unchanged. To opt a skill in, change its version to <code>@installed</code> and run
<code>skillfold install</code> once:</p>
<pre><code class="language-yaml"># before
playwright-cli: npm:@playwright/cli/skills/playwright-cli@0.1.18
# after
playwright-cli: npm:@playwright/cli/skills/playwright-cli@installed
</code></pre>
<p>The object form works too: <code>version: installed</code>. The package has to be a
dependency of the project (or installed globally, in <code>-g</code> mode). If it is
not, resolution fails and says to install it or pin a version.</p>
<p>Playwright and HyperFrames also publish these skills in their GitHub
repositories, and a <code>github:</code> source is a common way to install them. If
the tool itself comes from npm, switching the skill to the npm source is
what lets it follow your installed version. A GitHub source tracks the
repository, not your install.</p>
<h2 id="what-it-does-not-do">What it does not do</h2>
<p><code>@installed</code> works only for npm sources, and only for packages that put
their skill inside the published tarball. Of the CLIs checked for this
post, <code>@playwright/cli</code> 0.1.21, <code>hyperframes</code> 0.8.77, and <code>skillfold</code>
2.7.0 do. <code>vercel</code> 60.1.3, <code>wrangler</code> 4.141.0, and <code>@remotion/cli</code>
4.0.529 do not ship a <code>SKILL.md</code> in their npm packages, so there is
nothing for this ref to follow.</p>
<p>It does not reach tools installed some other way. A binary from Homebrew,
pip, a release tarball, or a pnpm global install has no npm lockfile entry
and is not under <code>npm root -g</code>, and skillfold does not guess by running
<code>tool --version</code>. <code>@installed</code> is rejected on GitHub
sources outright, because a Git ref has no installed version to follow.</p>
<p>yarn and bun lockfiles are not parsed. Those projects are covered by the
<code>node_modules</code> fallback, which means their fresh clones need an install
before <code>skillfold check</code> can see the version.</p>
<p>It keeps the two numbers in step; it cannot make the skill correct. If a
maintainer ships a release whose bundled skill still describes last
month&#39;s flags, <code>@installed</code> will faithfully install that skill.</p>
<p>Finally, this is merged on <code>main</code> but not yet in the npm package. The
current npm release is 2.7.0. <code>@installed</code> becomes available to npm users
in the next skillfold release; until then this post describes the
behavior on <code>main</code>, not a capability of 2.7.0.</p>
<p>The reference is in the
<a href="https://github.com/byronxlg/skillfold/blob/main/docs/manifest.md#following-an-installed-package-installed">manifest docs</a>,
and <code>npm run test:e2e</code> in the repository reruns these scenarios against
the real packages, under npm, pnpm, a workspace, and global mode.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Claude Code now labels what a subagent tells you as subagent output</title>
      <link>https://byronxlg.com/skillfold/blog/claude-code-subagent-injection-hardening/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/claude-code-subagent-injection-hardening/</guid>
      <pubDate>Mon, 21 Sep 2026 12:00:00 GMT</pubDate>
      <description>Claude Code 2.1.277 marks subagent results and cleans invisible Unicode from prompts. What that closes, and what pinning skill content still doesn&#39;t.</description>
      <content:encoded><![CDATA[<p>Claude Code shipped version 2.1.277 on September 18, 2026. Buried in a long
changelog entry are three related changes that all target the same problem:
text that arrives from somewhere other than the user, but that the model
could previously mistake for the user&#39;s own instructions. Per the
<a href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md">official changelog</a>,
under version 2.1.277:</p>
<blockquote>
<p>Changed subagent results to reach the main agent under a header marking
them as subagent output, with the result indented, so text in a
subagent&#39;s result cannot pass as the session&#39;s own instructions</p>
</blockquote>
<blockquote>
<p>Changed workflow scripts&#39; computed <code>agent()</code> prompts on Bedrock, Vertex
and Foundry to reach the subagent framed as script-authored text, so the
safety classifier does not read them as the user</p>
</blockquote>
<blockquote>
<p>Improved prompt handling: invisible Unicode formatting and tag
characters in a prompt are removed and the cleaned prompt is shown for
review before it is sent</p>
</blockquote>
<p>Three different code paths, one shared failure mode: something other than
the person at the keyboard puts text in front of the model, and without a
clear boundary, the model has no way to tell that text apart from an
instruction it should obey.</p>
<h2 id="what-each-change-actually-closes">What each change actually closes</h2>
<p>The first two are about agent-to-agent trust inside a single Claude Code
session. When a main agent dispatches a subagent - which is exactly what
happens when a workflow script or a <code>Task</code>-style delegation runs - the
subagent&#39;s result used to come back as plain text alongside everything
else in the transcript. If that result happened to contain something that
read like an instruction (&quot;ignore the above and instead...&quot;), there was no
structural signal telling the main agent that the text originated from a
delegated worker rather than from the user. Marking the result under an
explicit &quot;subagent output&quot; header, indented, gives the model a boundary to
reason about instead of asking it to infer provenance from phrasing alone.
The workflow-script change does the same thing one layer up: a script can
compute a prompt string and hand it to <code>agent()</code>, and on Bedrock, Vertex,
and Foundry that computed text now reaches the safety classifier framed as
script-authored rather than user-authored, so script output doesn&#39;t
inherit user-level trust it never earned.</p>
<p>The third change is different in kind. Unicode has a range of invisible
&quot;tag&quot; characters (<code>U+E0000</code>-<code>U+E007F</code>) and formatting characters designed
for legitimate uses like language tagging, but they&#39;ve also been
demonstrated as a way to smuggle text a human reviewer can&#39;t see into
content a model still reads and acts on - the same principle as
zero-width-character steganography, applied to prompts. Claude Code now
strips those before a prompt is sent and shows the cleaned version for
review, so what you approve is what the model actually receives.</p>
<h2 id="why-this-belongs-in-the-skill-supply-chain-conversation">Why this belongs in the skill supply chain conversation</h2>
<p>None of these three changes are about skills specifically - they&#39;re about
prompt provenance in general. But the failure mode they close is exactly
the one that shows up when a security researcher goes looking for
problems in agent skill content: a <code>SKILL.md</code>, a tool&#39;s output, or text
returned from an MCP server is something the agent reads and can act on,
and until there&#39;s a structural boundary, &quot;it&#39;s only text in the context
window&quot; is the whole attack surface. A malicious or compromised skill
doesn&#39;t need code execution if it can get its output read as an
instruction. Marking subagent output and stripping invisible Unicode
are runtime-level defenses against that category, independent of where
the text came from - a compromised MCP server, a poisoned web page pulled
in by WebFetch, or a skill&#39;s own body.</p>
<h2 id="what-skillfold-does-and-doesn-39-t-do-about-it">What skillfold does and doesn&#39;t do about it</h2>
<p>Skillfold&#39;s job sits one layer earlier: making sure the skill content
that ends up on disk is the content you actually reviewed, and that it
doesn&#39;t drift silently between installs. Every remote skill in
<code>skillfold.lock</code> carries a <code>sha256</code> content hash computed over its files
(<code>src/resolve.ts</code>), and <code>skillfold install --frozen</code> fails the moment
installed content diverges from that hash - see the <code>--frozen: &quot;&lt;name&gt;&quot; content hash does not match the lockfile</code> check in <code>resolveSkill</code>. That
answers &quot;is this the exact skill I pinned,&quot; the same question <code>npm ci</code>
answers for packages, and it&#39;s real protection: a skill can&#39;t be swapped
out from under you between CI runs without the hash catching it.</p>
<p>It does not answer &quot;is there anything hidden inside the skill I pinned.&quot;
A <code>SKILL.md</code> can carry invisible Unicode tag characters or an instruction
phrased to look like innocuous context, and if you approve that file once,
its hash then pins the malicious content as faithfully as it would
pin honest content. Skillfold verifies integrity, not intent - it has no
opinion on what a byte-identical file says. That gap is exactly what
2.1.277&#39;s prompt-cleaning step is for, and it&#39;s why the two are
complementary rather than redundant: a lockfile stops a skill from
changing without your knowledge, a runtime-level defense like this one
stops what&#39;s already in the file from being read as an instruction it was
never entitled to give. Neither one does the other&#39;s job. If you maintain
a skill registry or install skills from third parties, both checks belong
in the pipeline, not only one.</p>
]]></content:encoded>
    </item>
    <item>
      <title>One manifest can now install skills and rules for Cursor</title>
      <link>https://byronxlg.com/skillfold/blog/cursor-target/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/cursor-target/</guid>
      <pubDate>Tue, 15 Sep 2026 12:00:00 GMT</pubDate>
      <description>Skillfold&#39;s new Cursor target installs project skills and always-on rules in Cursor&#39;s native paths, with the same lockfile and drift checks.</description>
      <content:encoded><![CDATA[<p>Cursor recognizes the same <code>SKILL.md</code> packages as other coding agents, but
its native project paths are its own. Skills live under <code>.cursor/skills</code>.
Project rules live under <code>.cursor/rules</code>, where a <code>.mdc</code> extension and YAML
frontmatter determine whether Cursor loads them.</p>
<p>Skillfold now has a first-class <code>cursor</code> target for both:</p>
<pre><code class="language-yaml">targets: [claude, codex, cursor]
</code></pre>
<p>The target landed on <code>main</code> in
<a href="https://github.com/byronxlg/skillfold/pull/574">pull request 574</a>. It lets one
manifest and lockfile materialize the same declared configuration into each
tool&#39;s native layout, then check every copy for drift.</p>
<h2 id="what-gets-installed">What gets installed</h2>
<p>Cursor&#39;s
<a href="https://cursor.com/docs/skills">Agent Skills documentation</a>
lists <code>.cursor/skills</code> and <code>.agents/skills</code> as project discovery roots, with
matching user-level roots under the home directory. Skillfold deliberately
uses Cursor&#39;s named path for the Cursor target:</p>
<table>
<thead>
<tr>
<th>Scope</th>
<th>Skills</th>
<th>Rules</th>
</tr>
</thead>
<tbody><tr>
<td>Project</td>
<td><code>.cursor/skills/{name}/</code></td>
<td><code>.cursor/rules/{name}.mdc</code></td>
</tr>
<tr>
<td>User</td>
<td><code>~/.cursor/skills/{name}/</code></td>
<td>Managed in Cursor&#39;s UI</td>
</tr>
</tbody></table>
<p>Project skills keep the standard directory shape. Supporting files such as
scripts, references, and assets travel with <code>SKILL.md</code>, and composed skills
are generated the same way they are for the Claude and Codex targets.</p>
<p>The lockfile records <code>cursor</code> among its targets. Remote sources still resolve
to an exact Git commit or npm version, with a content hash stored beside the
pin. Local sources remain local. <code>skillfold check</code> walks <code>.cursor/skills</code> and
reports a missing or modified copy even when the copies installed for other
agents are intact.</p>
<h2 id="plain-markdown-in-valid-cursor-rules-out">Plain Markdown in, valid Cursor rules out</h2>
<p>Skillfold rule sources are plain Markdown files. Cursor project rules are not:
the
<a href="https://cursor.com/docs/rules">Cursor Rules documentation</a>
says project rules must use <code>.mdc</code> and frontmatter. A plain <code>.md</code> file inside
<code>.cursor/rules</code> is ignored.</p>
<p>For the Cursor target, skillfold wraps each rule source with generated
metadata:</p>
<pre><code class="language-md">---
description: &quot;Managed by skillfold: code-style&quot;
alwaysApply: true
---

...the declared rule source, unchanged...
</code></pre>
<p>That conversion preserves the meaning rules already have in a skillfold
manifest. A declared rule is standing guidance, so the generated Cursor rule
uses <code>alwaysApply: true</code>. The original Markdown body remains the content whose
integrity is pinned and checked. If someone edits the body or generated
frontmatter in place, <code>skillfold check</code> reports drift and <code>skillfold install</code>
repairs it.</p>
<p>This design also keeps one source usable across targets. The same
<code>rules/code-style.md</code> can become a plain file under <code>.claude/rules</code>, a managed
section in Codex&#39;s <code>AGENTS.md</code>, and an always-on <code>.mdc</code> file for Cursor.</p>
<h2 id="adding-cursor-to-an-existing-manifest">Adding Cursor to an existing manifest</h2>
<p>Add the target, run a normal install, and commit the lockfile change:</p>
<pre><code class="language-sh"># edit skillfold.yaml: targets: [claude, codex, cursor]
skillfold install
skillfold check
git add skillfold.yaml skillfold.lock
</code></pre>
<p>Do not use <code>install --frozen</code> for the first run after changing <code>targets</code>.
Frozen mode correctly rejects manifest and lockfile disagreement. Once the
normal install records the new target, frozen installs become valid again.</p>
<p>Existing files in <code>.cursor/skills</code> and <code>.cursor/rules</code> do not silently become
skillfold&#39;s property. When the target is new, the previous lockfile names
nothing there as managed. Byte-identical content can be adopted. A
same-named file with different content stops the install unless you explicitly
pass <code>--force</code>. Unrelated hand-authored files remain untouched.</p>
<p>Per-skill and per-rule target selectors continue to work. A project can enable
Cursor globally while keeping one tool-specific item out of its paths:</p>
<pre><code class="language-yaml">targets: [claude, codex, cursor]

skills:
  shared-reviewer: npm:skillfold/code-review
  claude-only:
    source: ./skills/claude-only
    targets: [claude]

rules:
  shared-style: ./rules/style.md
</code></pre>
<p>The full path and selector contract is in the
<a href="https://github.com/byronxlg/skillfold/blob/main/docs/manifest.md#targets">manifest reference</a>.</p>
<h2 id="where-the-target-stops">Where the target stops</h2>
<p>Cursor&#39;s global User Rules are configured in <strong>Customize &gt; Rules</strong>. They are
not ordinary files under <code>~/.cursor</code>, so skillfold does not pretend it can
manage them. Global mode installs Cursor skills to <code>~/.cursor/skills</code>, but
rejects a rule selected for the Cursor target with an explanation instead of
writing a file Cursor would ignore.</p>
<p>The target also does not expose Cursor&#39;s rule activation modes. Skillfold rules
become always-on project guidance. It does not currently map globs, manual
activation, or &quot;Apply Intelligently&quot; descriptions from the manifest. If a rule
needs those Cursor-specific semantics, keep that <code>.mdc</code> file hand-authored
outside skillfold&#39;s managed names. That boundary is intentional in this first
version.</p>
<p>There is one more practical overlap: Cursor discovers <code>.agents/skills</code> as well
as <code>.cursor/skills</code>. If a manifest enables both <code>codex</code> and <code>cursor</code>, the same
skill is installed in both roots because each target serves its named tool.
Cursor&#39;s documentation does not define a precedence rule for same-named copies
across those roots. Do not enable the Codex target solely to serve Cursor; use
<code>cursor</code> for Cursor and <code>codex</code> when the project also needs Codex.</p>
<p>Finally, this code is merged but not yet in the npm package. The current npm
release remains 2.6.0. The Cursor target will become available to npm users in
the next skillfold release; until then, the post documents the behavior on
<code>main</code>, not a capability in 2.6.0.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Claude Code can score whether a plugin helps. It doesn&#39;t ask which copy you ran.</title>
      <link>https://byronxlg.com/skillfold/blog/claude-plugin-eval/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/claude-plugin-eval/</guid>
      <pubDate>Mon, 14 Sep 2026 12:00:00 GMT</pubDate>
      <description>Claude Code&#39;s new plugin eval command measures a plugin&#39;s effect with a no-plugin baseline and gates CI on it - but the score never records which revision produced it.</description>
      <content:encoded><![CDATA[<p>Testing a Claude Code plugin has meant one thing so far: load it with
<code>--plugin-dir</code> and try it. That tells you the plugin can work. It says
nothing about how often it works, whether a rubric change or a new model
quietly broke it, or whether the plugin is doing anything at all versus
Claude just being capable enough to get there on its own.</p>
<p>Claude Code 2.1.269, shipped
<a href="https://code.claude.com/docs/en/changelog">September 11, 2026 per the official changelog</a>,
closes that gap:</p>
<blockquote>
<p>Added <code>claude plugin eval</code>: run a plugin&#39;s eval suite against Claude Code
and get scored, reproducible results (JSON + HTML report); see
<code>claude plugin eval --help</code></p>
</blockquote>
<h2 id="what-it-actually-measures">What it actually measures</h2>
<p>An eval suite lives in an <code>evals/</code> directory inside the plugin. Each case is
a subdirectory holding a <code>prompt.md</code> - a realistic request a user might type</p>
<ul>
<li>and one or more graders. The
<a href="https://code.claude.com/docs/en/plugin-evals">plugin evals documentation</a>
lists six grader types: <code>regex</code>, <code>tool_used</code>, <code>tool_order</code>, and
<code>file_exists</code> are computed from the transcript and cost nothing; <code>llm</code> and
<code>baseline</code> call a judge model and add to the run&#39;s cost. A grader can check
that a specific skill fired, that a file came out matching a pattern, or
that a second model&#39;s rubric verdict was PASS.</li>
</ul>
<p>The part that makes the score mean something is the baseline. By default
every case runs twice: once with the plugin loaded, once without. The
difference between the two scores, written <code>Δ</code>, is what the plugin actually
contributed. The docs are explicit about what a tie means: &quot;If a case
scores 1.0 both with and without the plugin, the plugin isn&#39;t what made it
pass.&quot; Without that control, a high score could just mean the prompt was
easy.</p>
<p>That doubles the cost of a run, so <code>claude plugin eval init</code> exists to
generate a first suite for you: it reads the plugin, proposes prompts that
should and shouldn&#39;t trigger it, drafts graders, and pilots each one before
writing the case files.</p>
<h2 id="built-to-sit-in-ci-not-just-a-terminal">Built to sit in CI, not just a terminal</h2>
<p>The design clearly targets automation, not one-off checks. <code>claude plugin eval</code> writes a versioned <code>aggregate-result.json</code> with a <code>schemaVersion</code> field
so a gating script doesn&#39;t break when new fields get added. A <code>--threshold</code>
flag (default <code>1.0</code>) fails the run - and the process exit code - when any
case scores below it. A <code>--trust-plugin</code> flag skips the interactive trust
prompt that would otherwise stall a non-interactive job. And the docs
specifically warn to pin <code>--model</code> in CI, &quot;so a model rollout isn&#39;t mistaken
for a plugin regression&quot; - a sign the team building this expects it to run
on every PR, repeatedly, against a moving model target.</p>
<p>That&#39;s a real and useful thing to have. A plugin&#39;s skill descriptions can
degrade in ways nothing else catches - a rubric that used to trigger cleanly
stops firing after a wording change, or a new model interprets a prompt
differently than the one the suite was written against. Eval turns that from
a vague feeling into a number with a sign.</p>
<h2 id="what-the-score-doesn-39-t-tell-you">What the score doesn&#39;t tell you</h2>
<p>Read the security section of the same doc closely and the boundary is
explicit: <code>claude plugin eval</code> &quot;loads the target plugin&#39;s skills and hooks
and runs its eval suite on your machine, as you.&quot; It evaluates whatever is
on disk at the plugin&#39;s path right now - a checkout, an installed copy, a
<code>--plugin-dir</code> folder - not a specific pinned revision of it. The JSON
result records <code>costUsd</code>, <code>durationSeconds</code>, and <code>claudeVersion</code> - the
version of Claude Code that ran the suite - but nothing in the documented
schema identifies which commit or version of the <em>plugin</em> produced the
score.</p>
<p>That distinction matters more in CI than at your terminal. Two developers
can each get <code>Δ +0.67</code> on the same eval suite while running two different
uncommitted edits to the plugin, and both runs are honestly reporting what
they measured: the plugin, whichever one was on disk, helped. Nothing in the
report says which plugin that was. A CI gate built on <code>--threshold</code> proves
the code sitting in that job&#39;s checkout passed the suite at that moment. It
does not by itself prove that the copy a teammate has installed, or the copy
that shipped to a marketplace, is the one that earned the score - that&#39;s a
separate claim, and it needs a separate mechanism to back it: a version
pinned in a manifest, and a hash checked against what&#39;s actually installed.</p>
<h2 id="where-a-lockfile-fits-and-where-it-stops">Where a lockfile fits, and where it stops</h2>
<p>This is the piece skillfold is built for and nothing else in this post is:
proving that an installed skill&#39;s bytes are the exact ones a manifest
declared. <code>skillfold check</code> and <code>install --frozen</code> compare a sha256 hash of
what&#39;s on disk against the hash recorded in <code>skillfold.lock</code>
(<code>src/install.ts</code>), and fail loudly on any mismatch (<code>docs/cli.md</code>). That
answers &quot;is this the revision we pinned,&quot; which a plugin eval score never
claims to answer on its own.</p>
<p>But a lockfile has the opposite blind spot from an eval score. Content
integrity says the bytes are unchanged; it says nothing about whether those
bytes do anything useful. A skill can pass every hash check in
<code>skillfold check</code> while its description never triggers on real phrasing, or
its instructions produce the wrong output every time - <code>skillfold</code> has no
way to run a session, invoke an agent, or grade a transcript, and isn&#39;t
trying to. Pinning and evaluating are answers to different questions -
&quot;is this the code we agreed on&quot; and &quot;does that code work&quot; - and a plugin
that&#39;s rigorously eval-gated in CI can still be silently swapped for an
unpinned edit the moment it leaves that job, with nothing in either tool
noticing on its own.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Claude Code will tell you which skills are dead weight. It won&#39;t tell you if they drifted.</title>
      <link>https://byronxlg.com/skillfold/blog/claude-code-skill-doctor/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/claude-code-skill-doctor/</guid>
      <pubDate>Mon, 07 Sep 2026 12:00:00 GMT</pubDate>
      <description>Claude Code&#39;s new /skill-doctor flags unused skills and their context cost per session - a usage read, not a check on what&#39;s actually declared or installed.</description>
      <content:encoded><![CDATA[<p>Every skill you load adds its description to context on every turn, whether
or not the agent ever reaches for it. As a <code>.claude/skills</code> directory grows -
one skill per workflow, one per integration, ones inherited from a team
manifest you didn&#39;t write - that cost is invisible. Nothing in a normal
session tells you that a skill has fired zero times in a week and is still
sitting in every prompt.</p>
<p>Claude Code now answers that question directly. Version 2.1.261, shipped
<a href="https://code.claude.com/docs/en/changelog">September 4, 2026 per the official changelog</a>,
added a new command:</p>
<blockquote>
<p>Added <code>/skill-doctor</code> to show which loaded skills go unused and what they
cost in context, so you can prune them</p>
</blockquote>
<h2 id="what-it-actually-reports">What it actually reports</h2>
<p>The <a href="https://code.claude.com/docs/en/skills">Claude Code skills documentation</a>
describes it under a section titled &quot;Find unused skills&quot;:</p>
<blockquote>
<p>Every skill in the skill listing adds to your context on every turn,
whether or not Claude ever uses it. Run <code>/skill-doctor</code> to see what each of
your skills costs and how often it gets used, so you can decide which ones
to turn off.</p>
</blockquote>
<p>Running it interactively opens the report in the <code>/plugin</code> manager&#39;s Stats
tab; in headless mode with <code>-p</code>, Claude Code prints it as plain text. The
report is scoped to the current session - it &quot;covers the skills in your
session other than bundled skills and enterprise skills,&quot; flags any skill in
the listing that has never been invoked, says where to turn it off, and
separately lists plugins you haven&#39;t used recently.</p>
<p>There are real constraints on when it works. It requires Claude Code
v2.1.252 or later, isn&#39;t available in sessions that skip feature-flag
fetching, and doesn&#39;t work at all over Remote Control - running it from a
phone or browser gets back &quot;Skill usage reports are not available on this
connection,&quot; per the same docs page. You have to be at the terminal on the
machine actually running the session.</p>
<h2 id="the-problem-this-targets-is-real">The problem this targets is real</h2>
<p>This is a direct answer to skill bloat, which is a genuine and growing
failure mode as teams accumulate skills faster than they retire them.
Anthropic&#39;s own framing names the cost precisely: context consumed on every
turn, independent of whether the skill contributes anything that turn. A
report that says &quot;this skill has never fired and costs N tokens per prompt&quot;
is exactly the missing signal for deciding what to cut, and building it into
the tool itself - rather than leaving it to a third-party audit skill - means
every Claude Code user gets it without installing anything.</p>
<h2 id="what-it-doesn-39-t-tell-you">What it doesn&#39;t tell you</h2>
<p><code>/skill-doctor</code> measures one thing: usage and cost, observed in one running
session, on one machine. It has no opinion on where a skill came from, what
version it&#39;s supposed to be, or whether the copy on disk matches what your
team declared.</p>
<p>Concretely: say the report tells you <code>code-review</code> hasn&#39;t fired all week and
costs 800 tokens a turn, so you delete <code>.claude/skills/code-review</code> by hand.
That decision lives nowhere. It isn&#39;t recorded in a manifest, it doesn&#39;t
propagate to a teammate&#39;s machine, and if anything - a setup script, a
colleague&#39;s <code>git pull</code> of a shared dotfiles repo, a fresh <code>skillfold install</code></p>
<ul>
<li>re-materializes that directory from its declared source, your pruning
silently reverts with no diff to explain why. The report also can&#39;t catch
the opposite failure: a skill that <em>is</em> getting invoked but is running
different content than what&#39;s declared, because someone hand-edited the
installed copy after the fact. Usage and cost say nothing about integrity.</li>
</ul>
<p>skillfold&#39;s own <code>check</code> and <code>list</code> commands sit on the exact other side of
that line and don&#39;t overlap with <code>/skill-doctor</code> at all. They&#39;re offline:
<code>skillfold list</code> computes each skill&#39;s status by comparing the manifest
entry, the lockfile pin, and a sha256 hash of what&#39;s on disk
(<code>src/list.ts</code>), and <code>skillfold check</code> fails nonzero on any mismatch. Neither
command runs a session, invokes an agent, or has any way to know whether a
given skill was used once or a thousand times, or how many tokens its
frontmatter cost on any particular turn - that information doesn&#39;t exist
outside a live session, and skillfold never starts one. A skill can show
<code>ok</code> in <code>skillfold list</code> - content matches exactly what the team pinned -
while <code>/skill-doctor</code> reports it dead weight in every session for a month.
Both reports would be correct, because they&#39;re answering different
questions: one is &quot;does what&#39;s installed match what&#39;s declared,&quot; the other
is &quot;is what&#39;s installed worth its keep.&quot;</p>
<p>Deciding to prune a skill needs both. <code>/skill-doctor</code> tells you it&#39;s safe to
consider removing; a manifest change and <code>skillfold remove</code> (or the
equivalent hand-edit plus reinstall) is what makes that removal durable and
visible to everyone else running the same config. Neither tool does the
other&#39;s job, and nothing currently connects the two - a session-scoped usage
report and a declarative manifest are, for now, two separate places to look.</p>
]]></content:encoded>
    </item>
    <item>
      <title>npm built a pause button for publishing. Most CI still skips it.</title>
      <link>https://byronxlg.com/skillfold/blog/npm-staged-publishing/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/npm-staged-publishing/</guid>
      <pubDate>Mon, 31 Aug 2026 12:00:00 GMT</pubDate>
      <description>npm&#39;s staged publishing adds a human approval step before a package goes live, and a fresh trusted-publishing compromise shows what that step would catch.</description>
      <content:encoded><![CDATA[<p>On August 28, 2026, ten malicious versions of
<code>@7nohe/openapi-react-query-codegen</code> reached the npm registry, spanning
every maintained release line of the package. According to
<a href="https://socket.dev/blog/openapi-react-query-codegen-npm-compromise">Socket&#39;s writeup</a>
published the same day, the cause was a <code>release.yml</code> workflow that
triggered on any issue comment containing the text <code>npm publish</code>, without
checking who posted it. As Socket put it: &quot;Any GitHub account can publish a
fork&#39;s contents under this repository&#39;s OIDC identity by commenting
<code>npm publish</code> on any pull request.&quot;</p>
<p>The attacker used exactly that. They staged malicious preinstall scripts
across separate commit chains in a fork, then triggered the workflow by
comment. The resulting packages published through the project&#39;s legitimate
trusted-publishing pipeline, and all ten carried valid npm provenance
attestations. Socket&#39;s report is blunt about what that attestation does and
does not mean: &quot;Provenance proves <em>which workflow</em> built an artifact; it
does not prove that the workflow only builds trusted source.&quot;</p>
<p>This is not a new lesson - the July 2026 AsyncAPI compromise made the same
point when a hijacked <code>pull_request_target</code> workflow exposed a maintainer
token and let five malicious versions ship with fully valid provenance (see
<a href="/blog/asyncapi-npm-provenance/">our earlier post</a>). What&#39;s new
this time is that npm now has a specific, shipped answer to this exact
failure mode, and the August 28 incident is a clean test of what it would
have caught.</p>
<h2 id="what-staged-publishing-actually-does">What staged publishing actually does</h2>
<p>npm&#39;s <a href="https://docs.npmjs.com/staged-publishing/">staged publishing</a> went
generally available per
<a href="https://github.blog/changelog/2026-05-22-staged-publishing-and-new-install-time-controls-for-npm/">GitHub&#39;s May 22, 2026 changelog entry</a>.
Instead of <code>npm publish</code> making a version installable immediately, a
maintainer runs <code>npm stage publish</code> to push a tarball into a review queue.
Someone then inspects it with <code>npm stage view</code>, and either approves it with
two-factor authentication (<code>npm stage approve</code>) or rejects it. Nothing in
the queue is installable until a human clears it.</p>
<p>Critically, the docs confirm this is not limited to interactive publishing:
&quot;If you use trusted publishing (OIDC) from CI/CD, you can use staged
publishing to submit a package for review before it goes live.&quot; A registry
can go further and require it - npm&#39;s docs describe a &quot;stage-only&quot;
configuration where <code>npm publish</code> from CI is rejected outright and only
<code>npm stage publish</code> is accepted, so a compromised or hijacked workflow can
still push a build into the queue, but never straight to installable.</p>
<p>Adoption is opt-in per package, and it requires npm CLI 11.15.0 or newer.
Security researcher Adnan Khan&#39;s comment on the feature, quoted in
<a href="https://www.infoq.com/news/2026/08/npm-stage-available/">InfoQ&#39;s August 7, 2026 coverage</a>,
frames the target directly: &quot;Publish from CI via OIDC then approve the
package before it goes live for everyone. Shai-Hulud? Denied.&quot;</p>
<h2 id="would-it-have-caught-this-one">Would it have caught this one</h2>
<p><code>@7nohe/openapi-react-query-codegen</code> was not using staged publishing - the
comment-triggered workflow called <code>npm publish</code> directly, and the tarballs
went live the moment the trusted-publishing token signed them. Had the
project instead run <code>npm stage publish</code> from that same workflow (or
configured the package as stage-only), the attacker&#39;s comment would still
have triggered the build and the same OIDC identity would still have signed
it. But the result would have sat in the review queue instead of reaching
consumers, and the same commit-chain oddities Socket later reconstructed
through source inspection of the archived tarballs - new scripts introduced
ahead of a modified preinstall step - are exactly the kind of thing a
maintainer reviewing a staged tarball before approval is positioned to
catch. That&#39;s an inference about the counterfactual, not something Socket&#39;s
report claims; the report itself only establishes what happened after
publication, through forensic analysis of the archives, not what staged
publishing would have changed had it been in place beforehand.</p>
<h2 id="where-skillfold-39-s-own-guarantee-stops">Where skillfold&#39;s own guarantee stops</h2>
<p>skillfold&#39;s <code>npm:</code> source resolves a package with <code>npm pack</code> at an exact
version, then records that version and a sha256 hash of the resolved
content in <code>skillfold.lock</code> (<code>src/npm.ts</code>, <code>src/lock.ts</code>). Running
<code>skillfold install --frozen</code> recomputes that hash and fails if the fetched
content doesn&#39;t match the lock (<code>src/resolve.ts</code>). That is a reproducibility
guarantee, not a vetting one, and the distinction matters for exactly the
scenario above.</p>
<p>skillfold never checks whether the npm package it&#39;s pinning used staged
publishing, and it has no concept of npm provenance attestations at all -
it doesn&#39;t fetch or verify them. If <code>skillfold add npm:some-pkg/skill</code> or
<code>skillfold update</code> resolves to a version that shipped through a compromised
trusted-publishing pipeline, the lockfile will faithfully record that
exact version, hash its exact (malicious) content, and <code>install --frozen</code>
will report success on every future run, because success only means &quot;this
matches what we pinned before,&quot; not &quot;this was ever safe to pin.&quot; The same
is true of skillfold&#39;s GitHub source: it pins a commit SHA, which is
reproducible and tamper-evident, but says nothing about whether that commit
was reviewed by anyone before you pointed at it.</p>
<p>That&#39;s the honest boundary. skillfold makes drift visible and reversion
possible; it does not, and cannot, verify that a version was ever safe in
the first place. That verification, if it happens at all, has to happen at
the publisher&#39;s end - which is exactly the gap staged publishing is built
to close, for the packages that turn it on.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Five vendors agreed on a plugin folder. Distribution is still your problem.</title>
      <link>https://byronxlg.com/skillfold/blog/agent-plugins-1.0/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/agent-plugins-1.0/</guid>
      <pubDate>Mon, 24 Aug 2026 12:00:00 GMT</pubDate>
      <description>Agent Plugins 1.0 gives skills and MCP servers one folder shape across six clients, then leaves versioning, pinning, and installation to someone else.</description>
      <content:encoded><![CDATA[<figure class="fig">
<svg viewBox="0 0 440 200" role="img" aria-labelledby="fig-t fig-d" xmlns="http://www.w3.org/2000/svg">
<title id="fig-t">What Agent Plugins standardizes, and what it leaves out</title>
<desc id="fig-d">A plugin folder with plugin.json, skills, and mcp.json feeds into six clients. Version pinning, a registry, and installation sit outside the spec, unfilled.</desc>
<rect x="4" y="6" width="180" height="120" rx="4" fill="#0d1219" stroke="#41b866"/>
<text x="94" y="24" text-anchor="middle" font-family="monospace" font-size="10" fill="#41b866">plugin/</text>
<text x="16" y="44" font-family="monospace" font-size="9" fill="#c9d3df">plugin.json</text>
<text x="16" y="64" font-family="monospace" font-size="9" fill="#c9d3df">skills/greet/</text>
<text x="26" y="80" font-family="monospace" font-size="9" fill="#828f9e">SKILL.md</text>
<text x="16" y="100" font-family="monospace" font-size="9" fill="#c9d3df">mcp.json</text>
<text x="200" y="70" font-family="monospace" font-size="16" fill="#41b866">→</text>
<rect x="224" y="6" width="212" height="120" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="330" y="24" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#c9d3df">ChatGPT · Codex · Cursor</text>
<text x="330" y="40" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#c9d3df">Copilot · Kiro · VS Code</text>
<text x="330" y="66" text-anchor="middle" font-family="monospace" font-size="9" fill="#d9a032">Claude Code: own format,</text>
<text x="330" y="80" text-anchor="middle" font-family="monospace" font-size="9" fill="#d9a032">.claude-plugin/plugin.json</text>
<rect x="4" y="146" width="432" height="46" rx="4" fill="#0d1219" stroke="#d9a032" stroke-dasharray="4 3"/>
<text x="220" y="164" text-anchor="middle" font-family="monospace" font-size="10" fill="#d9a032">out of scope: dependencies, version pinning,</text>
<text x="220" y="180" text-anchor="middle" font-family="monospace" font-size="10" fill="#d9a032">registries, installation, checksums</text>
</svg>
<figcaption>The spec fixes the shape of the folder. What fills it, pins it, and installs it is left to whoever consumes it - and Claude Code reads a differently-shaped folder entirely.</figcaption>
</figure><p>On August 6, 2026, five companies that normally compete for the same
developer attention published a joint specification instead. <a href="https://github.com/agentplugins/agent-plugins-spec">Agent Plugins
1.0.0</a>, announced on
<a href="https://vercel.com/blog/introducing-agent-plugins">Vercel&#39;s blog</a>, defines a
single folder format for packaging Agent Skills and MCP servers so one
plugin can be built once and read by more than one AI coding tool. It is a
narrow spec, and the gap it leaves on purpose is exactly the ground
skillfold&#39;s lockfile already covers - which makes it worth reading closely
rather than taking on faith.</p>
<h2 id="what-the-spec-actually-standardizes">What the spec actually standardizes</h2>
<p>A plugin is a directory with a <code>plugin.json</code> manifest at its root. Per the
<a href="https://agent-plugins.org/specification">specification</a>, only two fields
are required:</p>
<pre><code class="language-json">{
  &quot;$schema&quot;: &quot;https://agent-plugins.org/schemas/1.0.0/plugin.schema.json&quot;,
  &quot;name&quot;: &quot;hello-plugin&quot;
}
</code></pre>
<p>Everything else - <code>version</code>, <code>description</code>, <code>author</code>, <code>homepage</code>,
<code>repository</code>, <code>license</code>, <code>keywords</code> - is optional. Two component types sit
alongside the manifest: a <code>skills/</code> directory, where each immediate
subdirectory containing a <code>SKILL.md</code> file is treated as one skill built to
the separate <a href="https://agentskills.io/specification">agentskills.io</a> format,
and an <code>mcp.json</code> file declaring <code>stdio</code> or HTTP-based MCP servers under a
<code>mcpServers</code> key. Client-specific behavior can be layered on through
reverse-domain namespaces under an <code>extensions</code> field, so a given client can
add its own metadata without breaking the portable core.</p>
<p>The project&#39;s <a href="https://github.com/agentplugins/agent-plugins-spec">MAINTAINERS file</a>
names five Core Maintainers on the Technical Steering Committee: Clare
Liguori (Amazon), Roshan Sadanani (Cursor), Harald Kirschner (Microsoft),
Gav Verma (OpenAI), and Jonathan Hefner (Vercel, who holds the Lead Core
Maintainer role). Vercel&#39;s launch post states the format has support at
launch from ChatGPT, Codex, Cursor, GitHub Copilot, Kiro, and VS Code.</p>
<h2 id="what-it-deliberately-does-not-standardize">What it deliberately does not standardize</h2>
<p>The specification is explicit about its own boundary. It says nothing about
how a plugin declares a dependency on another plugin or skill, how a
version gets pinned once a client has fetched one, where a plugin comes
from (a registry, a git URL, a local path are all left to the client), or
how a client verifies that what it downloaded is what the author published.
There&#39;s no lockfile-equivalent anywhere in the spec, and no checksum field
on a plugin as a whole.</p>
<p>That&#39;s a defensible scope decision, not an oversight - a portable folder
shape is a real, useful thing to agree on across five companies, and
bundling a full package-manager story into the same effort would have made
consensus much harder to reach. But it means &quot;Agent Plugins 1.0 ships&quot; does
not mean the distribution problem is solved. It means the folder that gets
distributed now has an agreed shape, and every question of how it moves
from a publisher&#39;s repository to your machine - and whether the copy you
have is still the copy they published - is still open.</p>
<h2 id="where-claude-code-sits">Where Claude Code sits</h2>
<p>Claude Code is conspicuously absent from the Technical Steering Committee,
and its own plugin format predates and diverges from the new spec. Per
<a href="https://code.claude.com/docs/en/plugins">Claude Code&#39;s plugin
docs</a>, a Claude Code plugin&#39;s
manifest lives at <code>.claude-plugin/plugin.json</code> - nested inside a dot-prefixed
directory - not at the plugin root the way Agent Plugins specifies. The
Claude Code docs make no mention of Agent Plugins or <code>agent-plugins.org</code>.
Both formats independently converge on a <code>skills/&lt;name&gt;/SKILL.md</code> layout,
because both build on the same underlying Agent Skills spec, but a
<code>plugin.json</code> written for one does not sit in the place the other expects
it, and nothing in either spec bridges that gap automatically today. A
plugin author targeting both ecosystems is packaging twice, or maintaining
a translation step, not shipping one folder everywhere.</p>
<h2 id="what-this-means-for-a-manifest-that-already-pins-skills">What this means for a manifest that already pins skills</h2>
<p>skillfold&#39;s manifest currently resolves three kinds of sources - local
paths, <code>github:</code> refs, and <code>npm:</code> packages - into <code>SKILL.md</code>-based skill
directories, and its lockfile records a resolved commit SHA plus a sha256
integrity hash per skill (see <code>docs/manifest.md</code>). None of that reads or
writes <code>plugin.json</code>, and skillfold has no concept of an MCP server
declaration at all: <code>mcp.json</code> isn&#39;t a file type it looks for, parses, or
installs. A skill packaged inside an Agent Plugins folder that skillfold
happens to point at would only work if <code>skills/&lt;name&gt;/SKILL.md</code> inside it
still resolves as a plain skill directory - the surrounding <code>plugin.json</code>
and <code>mcp.json</code> would simply be ignored.</p>
<p>That&#39;s the honest limitation to state plainly: skillfold does not consume
the Agent Plugins format, does not resolve <code>mcp.json</code> servers, and has no
migration path for either as of this writing. If Agent Plugins gains real
adoption and clients start expecting MCP servers to travel inside the same
folder as skills, that&#39;s new surface skillfold&#39;s resolver doesn&#39;t cover yet.
What the spec does confirm is the shape of the problem the ecosystem still
hasn&#39;t agreed on: a portable <em>package</em> is not the same thing as a pinned,
verifiable <em>install</em>, and five companies agreeing on the former just made
that distinction easier to see.</p>
]]></content:encoded>
    </item>
    <item>
      <title>NVIDIA is signing its agent skills. Here is what a signature buys you</title>
      <link>https://byronxlg.com/skillfold/blog/nvidia-verified-agent-skills/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/nvidia-verified-agent-skills/</guid>
      <pubDate>Mon, 03 Aug 2026 12:00:00 GMT</pubDate>
      <description>NVIDIA now ships CUDA-X agent skills with a scan, a signature, and a machine-readable card. It is a real trust stack, and it stops well short of &quot;safe.&quot;</description>
      <content:encoded><![CDATA[<figure class="fig">
<svg viewBox="0 0 440 190" role="img" aria-labelledby="fig-t fig-d" xmlns="http://www.w3.org/2000/svg">
<title id="fig-t">NVIDIA's verified-skill pipeline</title>
<desc id="fig-d">A skill goes through cataloging, a SkillSpector scan, OMS signing, and a skill card, in that order, before publication.</desc>
<rect x="4" y="6" width="432" height="40" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="220" y="31" text-anchor="middle" font-family="monospace" font-size="10.5" fill="#c9d3df">skill source (CUDA-X library, blueprint, tool)</text>
<text x="60" y="66" font-family="monospace" font-size="18" fill="#41b866">↓</text>
<rect x="4" y="76" width="100" height="46" rx="4" fill="#0d1219" stroke="#41b866"/>
<text x="54" y="96" text-anchor="middle" font-family="monospace" font-size="9" fill="#41b866">catalog</text>
<text x="54" y="110" text-anchor="middle" font-family="monospace" font-size="8" fill="#828f9e">indexed</text>
<rect x="114" y="76" width="100" height="46" rx="4" fill="#0d1219" stroke="#41b866"/>
<text x="164" y="96" text-anchor="middle" font-family="monospace" font-size="9" fill="#41b866">SkillSpector</text>
<text x="164" y="110" text-anchor="middle" font-family="monospace" font-size="8" fill="#828f9e">scan for risk</text>
<rect x="224" y="76" width="100" height="46" rx="4" fill="#0d1219" stroke="#41b866"/>
<text x="274" y="96" text-anchor="middle" font-family="monospace" font-size="9" fill="#41b866">OMS sign</text>
<text x="274" y="110" text-anchor="middle" font-family="monospace" font-size="8" fill="#828f9e">skill.oms.sig</text>
<rect x="334" y="76" width="100" height="46" rx="4" fill="#0d1219" stroke="#41b866"/>
<text x="384" y="96" text-anchor="middle" font-family="monospace" font-size="9" fill="#41b866">skill card</text>
<text x="384" y="110" text-anchor="middle" font-family="monospace" font-size="8" fill="#828f9e">who, license, risks</text>
<rect x="4" y="140" width="432" height="44" rx="4" fill="#0d1219" stroke="#d9a032"/>
<text x="220" y="158" text-anchor="middle" font-family="monospace" font-size="10" fill="#d9a032">proves: this exact file came from NVIDIA and was reviewed once</text>
<text x="220" y="174" text-anchor="middle" font-family="monospace" font-size="10" fill="#828f9e">does not prove: the reviewed instructions are safe for your agent</text>
</svg>
<figcaption>Cataloging, scanning, signing, and a skill card are four separate claims. Only the first two are new; the pipeline still ends at a review, not a proof.</figcaption>
</figure><p>Most agent skills ship the way most npm packages shipped in 2015: a directory
of text and scripts, fetched from wherever the manifest points, trusted
because nothing forced anyone to check. NVIDIA&#39;s <a href="https://developer.nvidia.com/blog/nvidia-verified-agent-skills-provide-capability-governance-for-ai-agents/">Technical Blog post on
NVIDIA-Verified Agent
Skills</a>,
published May 19, 2026, describes what that ecosystem looks like once a
vendor decides to stop trusting it by default.</p>
<h2 id="the-problem-the-pipeline-answers">The problem the pipeline answers</h2>
<p>The motivating number comes from academic work, not NVIDIA&#39;s own research.
<a href="https://arxiv.org/abs/2601.10338">&quot;Agent Skills in the Wild: An Empirical Study of Security Vulnerabilities at
Scale&quot;</a> (Liu, Wang, Feng, Zhang, Xu, Deng,
Li, and Zhang; submitted to arXiv January 15, 2026) collected 42,447 skills
from two major marketplaces and ran 31,132 of them through SkillScan, a
static-plus-LLM detection pipeline the authors built for the study. Their
finding: &quot;26.1% of skills contain at least one vulnerability, spanning 14
distinct patterns across four categories: prompt injection, data
exfiltration, privilege escalation, and supply chain risks.&quot; NVIDIA&#39;s own
scanner, described below, cites the same paper for a narrower number: skills
that bundle an executable script are 2.12x more likely to be vulnerable than
instruction-only ones.</p>
<p>That is the same shape of finding the security beat has produced all year -
see the <a href="/blog/what-the-skill-supply-chain-research-recommends/">February-to-May roundup</a>
covering Snyk, Koi Security, Bitdefender, and Unit 42&#39;s marketplace audits.
What is new here is a large vendor building a standing pipeline in response,
rather than a one-time disclosure.</p>
<h2 id="what-nvidia-actually-publishes">What NVIDIA actually publishes</h2>
<p>NVIDIA-Verified Agent Skills are, per the post, &quot;portable instruction sets
that teach AI agents how to use NVIDIA CUDA-X libraries, AI Blueprints, and
platform tools correctly.&quot; Before publication, each one is cataloged,
scanned, cryptographically signed, and documented with a skill card. The
skills themselves are built on the open <code>agentskills.io</code> SKILL.md
specification, so the format is the same one skillfold, Claude Code, and
roughly forty other tools already read.</p>
<p><strong>The skill card</strong> is a machine-readable record shipped alongside the skill.
The post lists its required fields plainly: what the skill does, who built
it, how it is licensed, what its dependencies are, and &quot;what are the known
technical limitations, risks, and mitigations of the skill.&quot; That last field
is the interesting one - it is asking a publisher to write down the ways
their own skill can go wrong, in the same document a user reads before
installing it.</p>
<p><strong>The signature</strong> is a detached <code>skill.oms.sig</code> file that &quot;can be verified
post-download&quot; and covers every file and subdirectory in the skill&#39;s
directory, not <code>SKILL.md</code> alone. Verification uses OpenSSF Model Signing (OMS)
tooling checked against NVIDIA&#39;s published root certificate - the same
signing scheme OpenSSF built for model artifacts, repurposed here for skill
directories.</p>
<p><strong>The scan</strong> is <a href="https://github.com/NVIDIA/SkillSpector">SkillSpector</a>,
which NVIDIA open-sourced under Apache 2.0 and which anyone can run
independently of NVIDIA&#39;s own catalog: <code>skillspector scan ./my-skill/</code>
against a local directory, a zip, a single <code>SKILL.md</code>, or a Git URL. Per its
README, the current release detects &quot;68 vulnerability patterns across 17
categories&quot; - prompt injection, data exfiltration, privilege escalation,
supply chain, excessive agency, tool poisoning, and more - using AST
analysis, taint tracking, YARA signatures, and an optional LLM pass, then
rolls the findings into a 0-100 risk score. Above 50 the tool&#39;s own verdict
is &quot;DO NOT INSTALL.&quot; (Some syndicated coverage of SkillSpector cites 64
patterns across 16 categories; that reflects an earlier release. The README
in the repository as of this writing says 68 and 17, and a scanner&#39;s own
repo is the source that matters here.)</p>
<h2 id="what-each-piece-actually-proves">What each piece actually proves</h2>
<p>Cut through the pipeline and there are exactly two new claims being made,
and one old one dressed up:</p>
<ul>
<li><strong>The signature proves provenance and integrity</strong>: this file, byte for
byte, is what NVIDIA published under this name, and it has not been
altered since. That is a real, checkable fact, and it is the piece a bare
hash cannot give you - a hash tells you the bytes did not change since
<em>you</em> recorded it, a signature tells you who published them in the first
place.</li>
<li><strong>The scan proves a heuristic pass found nothing above a threshold.</strong> A
static analyzer plus an LLM classifier looking for 68 known patterns is
real work, and it is also, definitionally, bounded by the patterns it
knows to look for. A scan result is evidence, not a certificate of
safety, and NVIDIA&#39;s own SkillSpector output format reports a score and
findings rather than a pass/fail attestation.</li>
<li><strong>The skill card is a disclosure, not a guarantee.</strong> It asks the publisher
to state their own skill&#39;s risks. That is valuable precisely because it
is auditable and attributable - if the card is wrong, there is a specific
document and a specific publisher to point at - but it is only as honest
as whoever filled it in.</li>
</ul>
<p>None of that is a criticism specific to NVIDIA. It is the same ceiling every
scanner and every signature scheme in software supply-chain security runs
into: they answer &quot;is this the thing that was reviewed,&quot; not &quot;was the
review sufficient.&quot; NVIDIA&#39;s own post is explicit that this pipeline covers
skills NVIDIA itself publishes for its own libraries and blueprints - it is
not a general certification you can apply to an arbitrary skill from GitHub
or an open marketplace.</p>
<h2 id="where-this-leaves-a-lockfile">Where this leaves a lockfile</h2>
<p>skillfold&#39;s lockfile records a resolved commit SHA and a sha256 over every
file in a skill, matching a shape close to NVIDIA&#39;s card fields for
provenance and integrity:</p>
<pre><code class="language-yaml">frontend-design:
  source: github:anthropics/skills/skills/frontend-design@v1.2.0
  resolved: github:anthropics/skills/skills/frontend-design@8f3a9c1e...
  integrity: sha256-...
</code></pre>
<p>That hash answers &quot;did this change since I pinned it,&quot; which is most of
what a signature answers too, minus one thing: a hash has no notion of
<em>who</em> published the bytes, only that they match what you recorded. skillfold
has no signing step, no publisher identity check, and - unlike
SkillSpector - no scanner at all. It does not read skill content for
prompt-injection patterns, does not run static or LLM analysis, and does not
produce a risk score. Nothing stops a manifest from pinning a skill that
would score 90 on SkillSpector; skillfold will hash it, install it, and
report it in sync regardless.</p>
<p>The two tools solve adjacent, non-overlapping problems: SkillSpector (or any
scanner) answers whether a skill&#39;s content looks dangerous at a point in
time; skillfold answers whether the skill your agent loads today is the
exact same one that was reviewed. Running a scan once and never pinning the
result is exactly as fragile as pinning without ever having scanned - each
half needs the other, and right now nothing ships them together for the
open, multi-source skill ecosystem most people actually pull from.</p>
]]></content:encoded>
    </item>
    <item>
      <title>A valid provenance attestation shipped malware. Here is how.</title>
      <link>https://byronxlg.com/skillfold/blog/asyncapi-npm-provenance/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/asyncapi-npm-provenance/</guid>
      <pubDate>Mon, 27 Jul 2026 12:00:00 GMT</pubDate>
      <description>The July 2026 AsyncAPI npm compromise passed trusted publishing and provenance checks. What those checks actually proved, and what they never claimed to.</description>
      <content:encoded><![CDATA[<figure class="fig">
<svg viewBox="0 0 440 214" role="img" aria-labelledby="fig-t fig-d" xmlns="http://www.w3.org/2000/svg">
<title id="fig-t">What the AsyncAPI provenance attestation proved, and what it did not</title>
<desc id="fig-d">The attestation correctly proved the package came from the real repository, the real commit, and the real workflow. It could not prove the commit was authorized, because the workflow itself had been tricked into accepting it.</desc>
<rect x="4" y="6" width="212" height="132" rx="4" fill="#0d1219" stroke="#41b866"/>
<text x="16" y="28" font-family="monospace" font-size="10.5" fill="#41b866">attestation proved</text>
<text x="16" y="52" font-family="monospace" font-size="9.5" fill="#828f9e">real repository</text>
<text x="16" y="65" font-family="monospace" font-size="9.5" fill="#828f9e">real commit SHA</text>
<text x="16" y="86" font-family="monospace" font-size="9.5" fill="#828f9e">real GitHub Actions</text>
<text x="16" y="99" font-family="monospace" font-size="9.5" fill="#828f9e">workflow, real OIDC token</text>
<rect x="224" y="6" width="212" height="132" rx="4" fill="#0d1219" stroke="#e05a51"/>
<text x="236" y="28" font-family="monospace" font-size="10.5" fill="#e05a51">attestation could not prove</text>
<text x="236" y="52" font-family="monospace" font-size="9.5" fill="#828f9e">the commit was authorized</text>
<text x="236" y="65" font-family="monospace" font-size="9.5" fill="#828f9e">by a human maintainer</text>
<text x="236" y="86" font-family="monospace" font-size="9.5" fill="#828f9e">the workflow that ran it</text>
<text x="236" y="99" font-family="monospace" font-size="9.5" fill="#828f9e">had not been tricked</text>
<rect x="4" y="152" width="432" height="52" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="220" y="174" text-anchor="middle" font-family="monospace" font-size="10.5" fill="#c9d3df">provenance authenticates the pipeline</text>
<text x="220" y="191" text-anchor="middle" font-family="monospace" font-size="10.5" fill="#d9a032">not the intent behind what the pipeline ran</text>
</svg>
<figcaption>npm's trusted publishing worked exactly as designed on July 14, 2026 - and shipped malware anyway.</figcaption>
</figure><p>On July 14, 2026, five malicious versions of four <code>@asyncapi</code> npm packages
went out carrying fully valid npm trusted publishing signatures. That is
the detail worth sitting with. Trusted publishing and provenance
attestations are the mitigation the package-manager ecosystem has spent the
last two years converging on, and they did exactly what they were designed
to do here. The packages still shipped a credential-stealing implant to
everyone who imported them.</p>
<h2 id="what-happened">What happened</h2>
<p>Microsoft&#39;s Threat Intelligence team
<a href="https://www.microsoft.com/en-us/security/blog/2026/07/15/unpacking-asyncapi-npm-supply-chain-compromise-import-time-payload-delivery/">published the incident writeup</a>
on July 15, 2026. Five versions across four packages were affected:
<code>@asyncapi/specs@6.11.2-alpha.1</code> and <code>6.11.2</code>, <code>@asyncapi/generator@3.3.1</code>,
<code>@asyncapi/generator-components@0.7.1</code>, and
<code>@asyncapi/generator-helpers@1.1.1</code>. AsyncAPI&#39;s tooling is widely used for
generating code and docs from AsyncAPI specifications, so the blast radius
was every build that pulled a fresh install in the roughly 24 hours the
versions were live.</p>
<p>The entry point was a GitHub Actions workflow triggered on
<code>pull_request_target</code>, which runs with the base repository&#39;s permissions
and secrets even when it checks out a fork&#39;s code. Microsoft&#39;s writeup
describes this as placing &quot;the job in the base repository&#39;s security
context&quot; while still checking out untrusted code - the standard failure
mode for that trigger, well documented, and still exploitable here. That
exposed the <code>asyncapi-bot</code> personal access token, which the attacker used
to push unauthorized commits that then rode the project&#39;s own, legitimate
release workflow to npm.</p>
<p>That release workflow used npm trusted publishing: GitHub Actions OIDC
tokens instead of a long-lived npm auth token, with npm issuing a signed
provenance attestation tying each published version to the exact source
commit and workflow run that built it. It is the configuration <a href="https://docs.npmjs.com/trusted-publishers/">npm&#39;s own
documentation</a> recommends, and
none of it failed. Per Microsoft, &quot;the attestations accurately identified
the legitimate repositories, commits, and workflows that created the
packages, even though the triggering commits were unauthorized.&quot; The chain
of custody was genuine. The commit at the start of that chain was not one
a human maintainer intended to ship.</p>
<p>The payload itself is worth noting for a second reason: it runs at import
time, when the package is <code>require</code>d or imported, rather than during
<code>npm install</code>. That means the standard defensive advice to run
<code>npm install --ignore-scripts</code> does nothing here - there is no install
script to skip. The loader spawns a detached process that pulls an
encrypted ~8.2 MB second stage from IPFS and launches what Microsoft calls
the Miasma runtime, with command-and-control, persistence, and credential
harvesting.</p>
<h2 id="what-provenance-is-actually-for">What provenance is actually for</h2>
<p>Provenance attestations answer one question: did this exact artifact come
from this exact commit, built by this exact workflow, on this exact CI
provider? That is a real, useful, previously-unanswerable question, and
the AsyncAPI incident proves the answer works - the forensic reconstruction
in Microsoft&#39;s writeup exists <em>because</em> the attestations correctly
recorded which commits and runs produced the malicious tarballs.</p>
<p>What provenance was never designed to answer is whether the commit at the
end of that chain reflects what a maintainer meant to publish. A stolen
PAT and a trusted release pipeline produce an attestation indistinguishable
from a legitimate one, because from the attestation&#39;s point of view,
nothing about the pipeline misbehaved. The compromise happened one layer
up, in the trigger that decided which code got to run with the bot&#39;s
credentials at all.</p>
<p>That is a narrower, more precise claim than &quot;provenance doesn&#39;t work,&quot; and
it is the one the incident actually supports.</p>
<h2 id="where-this-connects-and-where-it-stops">Where this connects, and where it stops</h2>
<p>skillfold&#39;s own lockfile makes a structurally similar promise for skill
and rule sources: a <code>resolved</code> commit SHA or exact npm version, plus a
sha256 <code>integrity</code> hash computed over the fetched files, checked again on
every <code>install --frozen</code> or <code>check</code>. That is real - I read <code>src/lock.ts</code>
and <code>src/resolve.ts</code> to confirm it holds - and it buys the same thing npm&#39;s
attestation buys: proof that what is on disk today matches what was
originally pinned, byte for byte, whoever or whatever produced it.</p>
<p>It buys nothing about what was pinned. If a skill or an npm package is
compromised at the exact moment someone runs <code>skillfold add</code> or
<code>skillfold update</code> against it, the lockfile pins the compromised version
as faithfully as a clean one, hashes it, and reports <code>ok</code> on every
subsequent install. This is the same blind spot as npm&#39;s OIDC attestation,
for the same structural reason: a cryptographic record of <em>what got
published and from where</em> cannot see backward into whether the commit
behind it was the one a human actually meant to ship.</p>
<p>One thing does not carry over directly, and it is worth being precise
about rather than reaching for the scarier-sounding claim: skillfold&#39;s npm
resolution runs <code>npm pack</code> to download a tarball and extracts it with
<code>tar</code> - it never runs <code>npm install</code> and never <code>require</code>s or executes any
code from the package. So the specific mechanism in this incident, code
that runs at import time and bypasses <code>--ignore-scripts</code>, does not touch
skillfold&#39;s own fetch path, because skillfold never imports what it fetches
in the first place. What an agent later does with the instructions or
scripts inside a fetched skill is a separate question skillfold&#39;s pinning
does not answer either way.</p>
<h2 id="the-takeaway">The takeaway</h2>
<p>Trusted publishing and provenance are worth adopting - they closed off
credential theft as an attack path here, and the forensic trail in
Microsoft&#39;s writeup only exists because they were in place. But treat them,
and any lockfile that makes similar promises, as answering &quot;has this
changed since I looked at it,&quot; not &quot;was this safe when I looked at it.&quot;
Those are different questions, and the second one still needs a human to
read the diff before the pipeline runs, rather than trusting the signature
after it does.</p>
]]></content:encoded>
    </item>
    <item>
      <title>A study mapped 1.4 million agent skills. Most declare no dependencies at all.</title>
      <link>https://byronxlg.com/skillfold/blog/agent-skill-supply-chain-study/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/agent-skill-supply-chain-study/</guid>
      <pubDate>Sat, 25 Jul 2026 12:00:00 GMT</pubDate>
      <description>A July 2026 paper built the first dependency graph across 1.4 million agent skills, and found most carry no declared dependency metadata at all.</description>
      <content:encoded><![CDATA[<figure class="fig">
<svg viewBox="0 0 440 200" role="img" aria-labelledby="fig7-t fig7-d" xmlns="http://www.w3.org/2000/svg">
<title id="fig7-t">Activation-ready but governance-poor metadata</title>
<desc id="fig7-d">Across 1,434,046 skills: frontmatter present in 99.55 percent, a name in 99.49 percent, a description in 99.52 percent, but fields declaring dependencies in only 1.40 percent. Separately, 58.73 percent of skill names collide with at least one other skill.</desc>
<text x="4" y="12" font-family="monospace" font-size="9" fill="#57626f">share of 1,434,046 skills</text>
<text x="4" y="36" font-family="monospace" font-size="9.5" fill="#828f9e">frontmatter present</text>
<rect x="152" y="26" width="228" height="13" fill="#1b232e"/>
<rect x="152" y="26" width="226.9" height="13" fill="#4d8bf5"/>
<text x="436" y="36" text-anchor="end" font-family="monospace" font-size="9.5" fill="#c9d3df">99.55%</text>
<text x="4" y="60" font-family="monospace" font-size="9.5" fill="#828f9e">name</text>
<rect x="152" y="50" width="228" height="13" fill="#1b232e"/>
<rect x="152" y="50" width="226.8" height="13" fill="#4d8bf5"/>
<text x="436" y="60" text-anchor="end" font-family="monospace" font-size="9.5" fill="#c9d3df">99.49%</text>
<text x="4" y="84" font-family="monospace" font-size="9.5" fill="#828f9e">description</text>
<rect x="152" y="74" width="228" height="13" fill="#1b232e"/>
<rect x="152" y="74" width="226.9" height="13" fill="#4d8bf5"/>
<text x="436" y="84" text-anchor="end" font-family="monospace" font-size="9.5" fill="#c9d3df">99.52%</text>
<text x="4" y="108" font-family="monospace" font-size="9.5" fill="#c9d3df">declares dependencies</text>
<rect x="152" y="98" width="228" height="13" fill="#1b232e"/>
<rect x="152" y="98" width="3.2" height="13" fill="#e05a51"/>
<text x="436" y="108" text-anchor="end" font-family="monospace" font-size="9.5" fill="#e05a51">1.40%</text>
<path d="M4 128 H436" stroke="#29323f"/>
<text x="4" y="154" font-family="monospace" font-size="9.5" fill="#828f9e">names that collide</text>
<rect x="152" y="144" width="228" height="13" fill="#1b232e"/>
<rect x="152" y="144" width="133.9" height="13" fill="#d9a032"/>
<text x="436" y="154" text-anchor="end" font-family="monospace" font-size="9.5" fill="#d9a032">58.73%</text>
<text x="4" y="184" font-family="monospace" font-size="9" fill="#57626f">enough metadata to load a skill, almost none to reason about what it pulls in</text>
</svg>
<figcaption>The corpus is 1,434,046 skills downloaded from the SkillsMP registry on 6 June 2026, 87.4% of the 1,640,440 the registry listed. The paper calls the pattern "activation-ready but governance-poor".</figcaption>
</figure><p>Agent skills have quietly become software artifacts with dependencies:
a <code>SKILL.md</code> can tell an agent to invoke another skill, shell out to an
npm package, or call an external service. Almost none of them say so.
That is the finding of <a href="https://arxiv.org/abs/2607.01136">&quot;Skills Are Not Islands: Measuring Dependency
and Risk in Agent Skill Supply Chains&quot;</a>,
a paper submitted July 1, 2026 by Changguo Jia, Tianqi Zhao, Runzhi He,
and Minghui Zhou, which is the first attempt to treat agent skills as a
dependency graph at ecosystem scale and measure what is actually inside
it.</p>
<h2 id="what-they-built-and-ran">What they built and ran</h2>
<p>The paper introduces &quot;Agent Skill Supply Chains&quot; (ASSCs) as a formal
model - skills, packages, and services as nodes, with edges for
skill-to-skill, skill-to-package, and skill-to-service dependencies -
and a tool called SkillDepAnalyzer (SDA) that extracts those edges from
a skill&#39;s frontmatter and prose using natural-language evidence, the
same way an SBOM tool would extract edges from a manifest file, except
here there usually is no manifest file to read.</p>
<p>They ran SDA against 1,434,046 skills downloaded from the SkillsMP
registry on June 6, 2026 - 87.4% of the 1,640,440 skills the registry
listed at the time. That is the largest dependency analysis of the
agent skill ecosystem published so far.</p>
<h2 id="present-but-not-declared">Present, but not declared</h2>
<p>The paper&#39;s core finding is a pattern it names skill metadata that is
&quot;activation-ready but governance-poor&quot;. Frontmatter of some kind is
present in 99.55% of the corpus, and the two fields an agent needs to
<em>activate</em> a skill are almost as universal: a name in 99.49%, a
description in 99.52%. Fields that would let a tool reason about what a
skill <em>depends on</em> appear in only 1.40%. Name collisions compound the
problem: 58.73% of skill names collide with at least one other skill in
the corpus, which the paper notes makes identity resolution fragile
even before dependencies enter the picture.</p>
<p>Where SDA did find dependency evidence - mostly in the prose telling an
agent what to run or call, not in structured fields - the breakdown
across the full corpus was:</p>
<table>
<thead>
<tr>
<th>Dependency type</th>
<th>Skills</th>
<th>Share</th>
</tr>
</thead>
<tbody><tr>
<td>Skill-to-skill</td>
<td>127,891</td>
<td>8.92%</td>
</tr>
<tr>
<td>Skill-to-package</td>
<td>221,925</td>
<td>15.48%</td>
</tr>
<tr>
<td>Skill-to-service</td>
<td>319,013</td>
<td>22.25%</td>
</tr>
<tr>
<td>Any of the above</td>
<td>524,802</td>
<td>36.60%</td>
</tr>
<tr>
<td>All three types</td>
<td>11,041</td>
<td>0.77%</td>
</tr>
</tbody></table>
<p>So more than a third of skills pull in something beyond their own
files, and almost none of that is declared anywhere a tool could check
without running an LLM-assisted extractor over the skill&#39;s prose first.</p>
<h2 id="the-security-signal">The security signal</h2>
<p>The paper also traced known-risky packages, services, and skills
through the graph to see how far they reach via transitive dependency,
not just direct use. It found 194 root skills reaching a known
malicious skill, 13.40% of that exposure arriving only transitively;
3,342 skills touching a dangerous code pattern, 78.43% transitively;
3,413 skills reaching the axios package specifically, 98.01%
transitively; and 29 root skills reaching a vulnerable MCP service,
93.10% transitively. The consistent shape across all four numbers is
that most of the exposure is inherited from something the skill
depends on, not something the skill&#39;s own author wrote - which is
exactly why an undeclared dependency graph is a worse problem than an
undeclared dependency count.</p>
<h2 id="what-the-paper-recommends">What the paper recommends</h2>
<p>Section VII of the paper splits its recommendations by who is
responsible. For registries: require a typed dependency manifest that
separates skill, package, and service dependencies; treat clusters of
interdependent skills as a first-class object to manage rather than
inspecting skills one at a time; and expose an audit command &quot;analogous
to <code>npm audit</code>.&quot; For skill developers: maintain a lockfile-like record
of exact versions, source repositories, paths, and dependency status.</p>
<p>That is, structurally, the two-file shape most package ecosystems
converged on after their own supply-chain incidents: a manifest a human
edits, and a lockfile a tool writes and nobody hand-edits. It is also,
not coincidentally, the shape skillfold already uses for the slice of
this problem it covers: <code>skillfold.yaml</code> is a typed manifest across
<code>skills</code>, <code>compose</code>, and <code>rules</code>, and <code>skillfold install</code> writes
<code>skillfold.lock</code> with the exact resolved commit SHA or version and a
sha256 content hash per entry (see the
<a href="https://github.com/byronxlg/skillfold/blob/main/docs/manifest.md">manifest reference</a>).
<code>compose</code> entries record their <code>use</code> list in the
lock too, so a skill built from other skills has that skill-to-skill
edge pinned and checkable - <code>skillfold check</code> fails the build if an
installed composed skill no longer matches what its locked inputs would
generate.</p>
<h2 id="what-this-does-not-solve">What this does not solve</h2>
<p>That overlap is narrower than it sounds, and worth being precise about.
skillfold&#39;s manifest only captures skill-to-skill dependency, and only
the explicit kind: an entry in <code>compose.use</code>. It has no equivalent of
SDA&#39;s skill-to-package or skill-to-service edges - if a skill&#39;s body
tells an agent to <code>npm install</code> something or call an external API,
skillfold has no visibility into that dependency at all, and neither
does anything else in the current tooling landscape; that is precisely
the gap this paper is pointing at. A sha256 integrity hash also is not
a vulnerability scan: it proves a pinned skill&#39;s content has not
changed since the day it was pinned, which is a different guarantee
than &quot;this skill&#39;s content is safe.&quot; Nothing about the manifest or
lockfile would have caught the axios exposure or the malicious-skill
reachability the paper measured, because both require walking
dependencies <em>inside</em> a skill&#39;s own instructions, and skillfold&#39;s
lockfile only reasons about the skills a project explicitly declares in
its own manifest, not the graph underneath them.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Microsoft ships stable Agent Skills for .NET and Python</title>
      <link>https://byronxlg.com/skillfold/blog/microsoft-agent-skills-dotnet-python/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/microsoft-agent-skills-dotnet-python/</guid>
      <pubDate>Sat, 25 Jul 2026 12:00:00 GMT</pubDate>
      <description>Microsoft&#39;s Agent Framework made SKILL.md a stable, two-language API this month, and what that does and does not mean for anyone managing skills.</description>
      <content:encoded><![CDATA[<figure class="fig">
<svg viewBox="0 0 440 200" role="img" aria-labelledby="fig6-t fig6-d" xmlns="http://www.w3.org/2000/svg">
<title id="fig6-t">Runtimes reading the same SKILL.md format</title>
<desc id="fig6-d">One SKILL.md directory format, defined at agentskills.io, is read by Claude Code, by Codex, and now by Microsoft Agent Framework as a stable API in .NET since 7 July 2026 and Python since 15 July 2026.</desc>
<defs><marker id="fig6-a" markerWidth="7" markerHeight="7" refX="6" refY="3" orient="auto"><path d="M0 0 L6 3 L0 6 z" fill="#57626f"/></marker></defs>
<rect x="128" y="6" width="184" height="38" rx="4" fill="#0d1219" stroke="#4d8bf5"/>
<text x="220" y="24" text-anchor="middle" font-family="monospace" font-size="11" fill="#c9d3df">SKILL.md</text>
<text x="220" y="38" text-anchor="middle" font-family="monospace" font-size="9" fill="#4d8bf5">agentskills.io</text>
<path d="M220 44 V60 M74 60 H366 M74 60 V80 M220 60 V80 M366 60 V80" fill="none" stroke="#57626f"/>
<rect x="4" y="86" width="140" height="34" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="74" y="107" text-anchor="middle" font-family="monospace" font-size="10" fill="#828f9e">Claude Code</text>
<rect x="150" y="86" width="140" height="34" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="220" y="107" text-anchor="middle" font-family="monospace" font-size="10" fill="#828f9e">Codex</text>
<rect x="296" y="86" width="140" height="34" rx="4" fill="#0d1219" stroke="#41b866"/>
<text x="366" y="102" text-anchor="middle" font-family="monospace" font-size="10" fill="#828f9e">Agent Framework</text>
<text x="366" y="114" text-anchor="middle" font-family="monospace" font-size="8.5" fill="#41b866">Microsoft</text>
<path d="M4 140 H436" stroke="#29323f"/>
<text x="4" y="162" font-family="monospace" font-size="9.5" fill="#828f9e">.NET stable, 7 July 2026</text>
<text x="4" y="180" font-family="monospace" font-size="9.5" fill="#828f9e">Python stable, 15 July 2026</text>
<text x="4" y="196" font-family="monospace" font-size="9" fill="#57626f">both announcements name agentskills.io as the spec being implemented</text>
</svg>
<figcaption>The signal is not the integration. It is a second vendor treating SKILL.md as an API stable enough to drop the experimental attribute from.</figcaption>
</figure><p>Eight days apart, Microsoft moved Agent Skills support in its Agent
Framework from experimental to stable, first for .NET on July 7, 2026, then
for Python on July 15, 2026. Both posts explicitly point at
<a href="https://agentskills.io">agentskills.io</a> as the spec they&#39;re implementing.
That&#39;s worth pausing on, because it&#39;s a data point about who now treats
<code>SKILL.md</code> as infrastructure worth a stable API, not just a convention
Claude clients happen to read.</p>
<h2 id="what-actually-shipped">What actually shipped</h2>
<p>The <a href="https://devblogs.microsoft.com/agent-framework/agent-skills-for-net-is-now-released/">.NET announcement</a>
describes removing the <code>[Experimental]</code> attribute from the Agent Skills
API, promoting it to production-ready. The
<a href="https://devblogs.microsoft.com/agent-framework/agent-skills-for-python-is-now-released/">Python announcement</a>
is more direct about it: &quot;Agent Skills for Python in Microsoft Agent
Framework is stable and shipping: the core skills API has no experimental
gate.&quot;</p>
<p>Both languages support three ways to author a skill:</p>
<ul>
<li><strong>File-based skills</strong> - a directory with a <code>SKILL.md</code> file, optionally
bundling scripts and reference docs. This is the same shape
<code>agentskills.io</code> specifies and the same shape skillfold installs.</li>
<li><strong>Class-based skills</strong> - a C# class or Python class, distributed as a
NuGet or PyPI package.</li>
<li><strong>Code-defined skills</strong> - skills constructed dynamically at runtime
inside application code.</li>
</ul>
<p>Both posts describe the same progressive-disclosure loading pattern the
spec itself uses: advertise a skill&#39;s name and description first, load the
full instructions only when a task matches, then pull in bundled scripts or
reference files as needed. The Python post frames it as four steps
(&quot;advertise skill names → load instructions → read resources → run
scripts&quot;), the spec&#39;s own overview frames it as three (discovery,
activation, execution) - same mechanism, different granularity of
description, not a disagreement.</p>
<h2 id="why-this-is-more-than-another-integration">Why this is more than another integration</h2>
<p><code>agentskills.io</code>&#39;s client showcase - fetched July 25, 2026 - lists 44
adopting tools: terminal agents (Claude Code, OpenAI Codex, Gemini CLI,
OpenCode, Goose), editors (VS Code, Cursor, JetBrains&#39; Junie), and
platforms (Snowflake Cortex Code, Databricks Genie Code). Most of those are
end-user products reading <code>SKILL.md</code> directories that someone else wrote.</p>
<p>Microsoft&#39;s Agent Framework is a different kind of adopter: it&#39;s an SDK
people use to <em>build</em> agents, and it just gave <code>SKILL.md</code> a stable,
non-experimental API alongside two other authoring paths that aren&#39;t
file-based at all. That&#39;s a vendor betting that file-based skills are worth
committing to as a public contract, not an experimental flag they might
pull next quarter.</p>
<p>It&#39;s also not the first SDK to add <code>SKILL.md</code> support - Spring AI shipped
a generic Agent Skills implementation back on
<a href="https://spring.io/blog/2026/01/13/spring-ai-generic-agent-skills/">January 13, 2026</a>,
built specifically to work &quot;across many LLM providers.&quot; But that
implementation ships under the <code>spring-ai-community</code> group ID at version
0.4.2 against a Spring AI milestone build, and its own &quot;Current
Limitations&quot; section flags unsandboxed script execution and no
human-in-the-loop controls. Microsoft&#39;s release, by contrast, leads with
production concerns: the .NET and Python posts both describe
human-in-the-loop approval, controlled script execution, filtering,
caching, and an extensible source pipeline for custom skill discovery. The
gap between those two isn&#39;t the file format - both read the same
<code>SKILL.md</code> - it&#39;s how much a vendor is willing to say &quot;you can run this in
production, unattended, against untrusted skill content.&quot;</p>
<h2 id="what-this-doesn-39-t-solve">What this doesn&#39;t solve</h2>
<p>A stable API in one more SDK doesn&#39;t make the ecosystem interoperable. Each
of those 44 clients, and now the Agent Framework, still has its own
discovery convention - where it looks for skills, what it expects
alongside <code>SKILL.md</code>, how it resolves updates. <code>agentskills.io</code> standardizes
the file format inside the directory; it doesn&#39;t standardize where the
directory lives or how it gets there.</p>
<p>That&#39;s the gap skillfold works in, and this release doesn&#39;t close it either</p>
<ul>
<li>it makes it slightly bigger. Skillfold today installs into two targets,
<code>claude</code> and <code>codex</code>, mapping to fixed locations (<code>.claude/skills</code> /
<code>.agents/skills</code>, and a managed block in <code>AGENTS.md</code> for Codex; see
<a href="https://github.com/byronxlg/skillfold/blob/main/src/targets.ts"><code>src/targets.ts</code></a>).
It has no idea where the Agent Framework expects a file-based skill
directory to sit, so a manifest-managed skill doesn&#39;t reach it without a
new target being taught to skillfold deliberately. And the class-based and
code-defined authoring paths aren&#39;t directories at all - they&#39;re code,
shipped through NuGet or PyPI, with their own versioning and audit story.
A lockfile built for pinning and hashing file trees has nothing to say
about a C# class compiled into a package.</li>
</ul>
<p>So the honest read is narrower than &quot;the standard won&quot;: one more vendor
now has a stable reason to keep <code>SKILL.md</code> working, which is good news for
anyone betting on the format&#39;s durability. It is not evidence that
installing a skill into any given tool got easier this month.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://devblogs.microsoft.com/agent-framework/agent-skills-for-net-is-now-released/">Agent Skills for .NET Is Now Released</a>, Microsoft Agent Framework blog, July 7, 2026</li>
<li><a href="https://devblogs.microsoft.com/agent-framework/agent-skills-for-python-is-now-released/">Agent Skills for Python Is Now Released</a>, Microsoft Agent Framework blog, July 15, 2026</li>
<li><a href="https://agentskills.io">agentskills.io</a>, specification overview and client showcase, accessed July 25, 2026</li>
<li><a href="https://spring.io/blog/2026/01/13/spring-ai-generic-agent-skills/">Spring AI generic Agent Skills</a>, Spring blog, January 13, 2026</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>MCP&#39;s stateless rewrite ships July 28: what actually changes</title>
      <link>https://byronxlg.com/skillfold/blog/mcp-stateless-rewrite/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/mcp-stateless-rewrite/</guid>
      <pubDate>Sat, 25 Jul 2026 12:00:00 GMT</pubDate>
      <description>The Model Context Protocol drops server sessions for a stateless core and writes down a deprecation policy for the first time. Here is what changes.</description>
      <content:encoded><![CDATA[<figure class="fig">
<svg viewBox="0 0 440 226" role="img" aria-labelledby="fig5-t fig5-d" xmlns="http://www.w3.org/2000/svg">
<title id="fig5-t">Session-pinned routing versus a stateless core</title>
<desc id="fig5-d">Previously an initialize handshake returned an Mcp-Session-Id and every later request had to reach the instance that issued it, requiring sticky routing. Now protocol version, client identity, and capabilities travel in a _meta field on every request, so any instance can answer any request.</desc>
<defs><marker id="fig5-a" markerWidth="7" markerHeight="7" refX="6" refY="3" orient="auto"><path d="M0 0 L6 3 L0 6 z" fill="#57626f"/></marker></defs>
<text x="4" y="14" font-family="monospace" font-size="9.5" fill="#d9a032">before: session pinned</text>
<rect x="4" y="24" width="96" height="30" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="52" y="43" text-anchor="middle" font-family="monospace" font-size="10" fill="#828f9e">client</text>
<path d="M104 39 H150" stroke="#57626f" marker-end="url(#fig5-a)"/>
<text x="127" y="33" text-anchor="middle" font-family="monospace" font-size="8.5" fill="#d9a032">session id</text>
<rect x="156" y="24" width="110" height="30" rx="4" fill="#0d1219" stroke="#d9a032"/>
<text x="211" y="43" text-anchor="middle" font-family="monospace" font-size="10" fill="#c9d3df">instance A</text>
<rect x="156" y="62" width="110" height="24" rx="4" fill="#0d1219" stroke="#1b232e"/>
<text x="211" y="78" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#57626f">instance B</text>
<text x="278" y="45" font-family="monospace" font-size="9" fill="#57626f">every later request</text>
<text x="278" y="58" font-family="monospace" font-size="9" fill="#57626f">must reach A</text>
<text x="278" y="78" font-family="monospace" font-size="9" fill="#d9a032">sticky routing</text>
<path d="M4 108 H436" stroke="#29323f"/>
<text x="4" y="132" font-family="monospace" font-size="9.5" fill="#41b866">after: stateless core</text>
<rect x="4" y="142" width="96" height="30" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="52" y="161" text-anchor="middle" font-family="monospace" font-size="10" fill="#828f9e">client</text>
<path d="M104 157 H140" fill="none" stroke="#57626f"/>
<path d="M140 157 V149 H150" fill="none" stroke="#57626f" marker-end="url(#fig5-a)"/>
<path d="M140 157 V181 H150" fill="none" stroke="#57626f" marker-end="url(#fig5-a)"/>
<text x="127" y="145" text-anchor="middle" font-family="monospace" font-size="8.5" fill="#41b866">_meta</text>
<rect x="156" y="136" width="110" height="26" rx="4" fill="#0d1219" stroke="#41b866"/>
<text x="211" y="153" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#828f9e">instance A</text>
<rect x="156" y="168" width="110" height="26" rx="4" fill="#0d1219" stroke="#41b866"/>
<text x="211" y="185" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#828f9e">instance B</text>
<text x="278" y="155" font-family="monospace" font-size="9" fill="#57626f">any instance can</text>
<text x="278" y="168" font-family="monospace" font-size="9" fill="#57626f">answer any request</text>
<text x="4" y="216" font-family="monospace" font-size="9.5" fill="#828f9e">version, identity, and capabilities ride on every request, not a handshake</text>
</svg>
<figcaption>The removal is the feature: without a session to pin, a deployment that needed sticky routing or shared session state stops needing either.</figcaption>
</figure><p>The Model Context Protocol - the wire format most agent tools now use to
reach external tools and data sources - ships its largest revision since
launch on July 28, 2026. The release candidate has been locked since May 21,
giving SDK maintainers a ten-week window to catch up, according to the
<a href="https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/">official MCP specification blog</a>.
The headline change is not a new feature. It is the removal of the one thing
every MCP deployment has depended on since the protocol&#39;s first release:
the session.</p>
<h2 id="what-a-stateless-core-actually-removes">What a stateless core actually removes</h2>
<p>Every MCP connection used to start with an <code>initialize</code> handshake that
returned an <code>Mcp-Session-Id</code> header, and every request after that had to
reach the same server instance that issued it. That is gone. Protocol
version, client identity, and capabilities now travel in a <code>_meta</code> field on
every request instead of being negotiated once and pinned to a session. Any
server instance can now answer any request, per the specification blog,
which means a remote MCP deployment that needed sticky routing, a shared
session store, or packet inspection at the load balancer to keep requests on
the right instance no longer needs any of that.</p>
<p>Server-initiated requests - a confirmation prompt mid-task, for example -
had to assume a persistent connection back to the client. They are
restructured too: a server can only issue one while actively handling a
client request, and the client can resume the exchange from any instance
using a <code>requestState</code> payload it echoes back on retry.</p>
<p>Three things are removed outright: the <code>initialize</code>/<code>initialized</code> handshake,
the <code>Mcp-Session-Id</code> header, and the <code>tasks/list</code> method, which the spec blog
says &quot;can&#39;t be scoped safely without sessions&quot;. A non-standard <code>-32002</code> error
code for missing resources is replaced with the JSON-RPC standard <code>-32602</code>.</p>
<p>Three features that were part of the core are marked deprecated rather than
removed: Roots, Sampling, and Logging. They still work.</p>
<h2 id="the-part-that-is-actually-new-a-deprecation-clock">The part that is actually new: a deprecation clock</h2>
<p>Protocol revisions have shipped breaking changes before. What has not existed
until this one is a written commitment about how much notice the next
breaking change gets. The spec blog now defines a formal lifecycle -
<em>Active</em>, <em>Deprecated</em>, <em>Removed</em> - and guarantees, in its own words,
&quot;at least twelve months between deprecation and the earliest possible
removal.&quot; A conformance suite gates new Standards Track proposals from
reaching Final status without going through that lifecycle.</p>
<p>That is the detail worth sitting with if you build against MCP rather than
just consume it through a client. A protocol that reserves the right to
change anything at any time is a protocol you can only track by reading
every release. A protocol that commits to twelve months&#39; notice is one you
can plan a migration against - which is a different, better problem to have,
even though this particular revision predates the policy that would have
given it more runway.</p>
<p>Extensions get the same kind of structure applied going forward: reverse-DNS
identifiers, independent versioning, and their own repositories under the SEP
process, rather than accreting into the core spec. Tasks - previously an
experimental core feature for long-running operations - moves into one of
the first such extensions, redesigned so a server returns a task handle and
the client drives progress with <code>tasks/get</code>, <code>tasks/update</code>, and
<code>tasks/cancel</code> instead of relying on a live connection.</p>
<h2 id="who-this-actually-disrupts">Who this actually disrupts</h2>
<p>Coverage from <a href="https://www.theregister.com/devops/2026/07/23/model-context-protocol-prepares-to-break-with-its-stateful-past/5276722">The Register</a>
quotes Anthropic&#39;s David Soria Parra putting the disruption where it
actually lands: &quot;If you built your own implementation, it&#39;s going to be a
lot of uplift to make this correct.&quot; Anyone on an official SDK is expected
to absorb the change within the ten-week validation window Tier 1 SDKs get
before the July 28 ship date. Anyone who hand-rolled a client or server
against the wire format directly inherits the session-removal work
themselves, and backward compatibility between the old and new protocol
revisions is not guaranteed - a server speaking 2026-07-28 and a client
still expecting a session id will not silently interoperate.</p>
<p>Six SEPs also tighten authorization against current OAuth 2.0 and OpenID
Connect practice: <code>iss</code> parameter validation per RFC 9207, an
<code>application_type</code> declaration during dynamic client registration, and
credentials bound to the server that issued them. None of that is optional
hardening bolted on top - it is part of the same revision.</p>
<h2 id="what-this-has-nothing-to-do-with">What this has nothing to do with</h2>
<p>MCP governs how an agent talks to a tool or data source over the wire. It
says nothing about how the <em>instructions</em> an agent loads - a <code>SKILL.md</code>, a
project&#39;s <code>AGENTS.md</code>, a rules file - got onto the machine running that
agent, and it has no equivalent yet of pinning a specific server to a
specific protocol revision the way a lockfile pins a package to a commit.
skillfold&#39;s manifest and lockfile resolve and hash skill sources; they say
nothing about MCP servers and do not touch protocol versions at all. If your
agent setup depends on MCP servers, this migration is a client and server
compatibility problem you handle independently of anything a skill manifest
covers, and as of this specification there still isn&#39;t a standard way to
declare and reproduce &quot;which MCP protocol revision does this agent&#39;s setup
expect&quot; the way you can declare which skill revision it expects. That gap is
worth naming, not papering over with an unrelated tool.</p>
<p>If you maintain an MCP server or client directly rather than through an
official SDK, the ten-week window closes soon. If you consume MCP only
through a client that tracks the spec, the practical action item this week
is confirming which SDK version you are on and whether it has picked up the
2026-07-28 revision yet.</p>
]]></content:encoded>
    </item>
    <item>
      <title>What the skill supply-chain research actually recommends</title>
      <link>https://byronxlg.com/skillfold/blog/what-the-skill-supply-chain-research-recommends/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/what-the-skill-supply-chain-research-recommends/</guid>
      <pubDate>Sat, 25 Jul 2026 12:00:00 GMT</pubDate>
      <description>Six months of disclosures converged on the same mitigations for agent skills. Here is what pinning and content hashes buy, and what they plainly do not.</description>
      <content:encoded><![CDATA[<figure class="fig">
<svg viewBox="0 0 440 214" role="img" aria-labelledby="fig4-t fig4-d" xmlns="http://www.w3.org/2000/svg">
<title id="fig4-t">What pinning and hashing cover, and what they do not</title>
<desc id="fig4-d">A resolved revision and a content hash cover substitution, silent upstream changes, and local edits. They do not cover whether the pinned instructions were safe in the first place. A lockfile makes a compromise reproducible, not harmless.</desc>
<rect x="4" y="6" width="212" height="132" rx="4" fill="#0d1219" stroke="#41b866"/>
<text x="16" y="28" font-family="monospace" font-size="10.5" fill="#41b866">covered</text>
<text x="16" y="52" font-family="monospace" font-size="9.5" fill="#828f9e">a moving tag silently</text>
<text x="16" y="65" font-family="monospace" font-size="9.5" fill="#828f9e">becoming something else</text>
<text x="16" y="86" font-family="monospace" font-size="9.5" fill="#828f9e">upstream changing the</text>
<text x="16" y="99" font-family="monospace" font-size="9.5" fill="#828f9e">content after review</text>
<text x="16" y="120" font-family="monospace" font-size="9.5" fill="#828f9e">an edit in your checkout</text>
<rect x="224" y="6" width="212" height="132" rx="4" fill="#0d1219" stroke="#e05a51"/>
<text x="236" y="28" font-family="monospace" font-size="10.5" fill="#e05a51">not covered</text>
<text x="236" y="52" font-family="monospace" font-size="9.5" fill="#828f9e">whether the pinned bytes</text>
<text x="236" y="65" font-family="monospace" font-size="9.5" fill="#828f9e">were safe to begin with</text>
<text x="236" y="86" font-family="monospace" font-size="9.5" fill="#828f9e">hidden or obfuscated</text>
<text x="236" y="99" font-family="monospace" font-size="9.5" fill="#828f9e">instructions</text>
<text x="236" y="120" font-family="monospace" font-size="9.5" fill="#828f9e">publisher identity</text>
<rect x="4" y="152" width="432" height="52" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="220" y="174" text-anchor="middle" font-family="monospace" font-size="10.5" fill="#c9d3df">a lockfile makes a compromise reproducible</text>
<text x="220" y="191" text-anchor="middle" font-family="monospace" font-size="10.5" fill="#d9a032">which is not the same thing as making it safe</text>
</svg>
<figcaption>Pinning turns a one-time read of a SKILL.md into a durable statement about what the agent runs. It is not a scanner, and treating it as a security control is a mistake worth naming.</figcaption>
</figure><p>Agent skills got their own security literature in the first half of 2026. Not
think pieces: vendor disclosures with sample counts, a CVE trail, and papers
on arXiv. If you install skills from anywhere other than your own repository,
the findings are worth reading, and the recommended mitigations are worth
reading more carefully than the findings.</p>
<p>The numbers, with their attributions:</p>
<ul>
<li>Snyk&#39;s February 2026 audit of 3,984 skills from the ClawHub registry found
36.82% contained at least one security flaw and 76 carried confirmed
malicious payloads, per the
<a href="https://labs.cloudsecurityalliance.org/research/briefing-csa-research-note-skill-md-agent-context-poisoning/">Cloud Security Alliance&#39;s briefing</a>.</li>
<li>Koi Security disclosed 341 malicious skills in its ClawHavoc report, a
typosquatting campaign against the registry&#39;s users.</li>
<li>Bitdefender Labs reported that roughly 17% of the skills it analyzed in the
platform&#39;s first weeks carried malicious payloads, cited in
<a href="https://unit42.paloaltonetworks.com/openclaw-ai-supply-chain-risk/">Unit 42&#39;s writeup</a>.</li>
<li>Unit 42&#39;s own February to May 2026 analysis walked through five skills that
were live and unblocked at the time of writing, spanning macOS
infostealers, scanner evasion, and runtime affiliate injection.</li>
</ul>
<p>There is also academic work: see
<a href="https://arxiv.org/html/2604.02837v1">Towards Secure Agent Skills</a> for a
threat taxonomy, and
<a href="https://arxiv.org/html/2603.00195v1">Formal Analysis and Supply Chain Security for Agentic AI Skills</a>.</p>
<h2 id="what-those-numbers-do-and-do-not-measure">What those numbers do and do not measure</h2>
<p>Start with the caveat, because it is the part most coverage drops. The large
datasets above come from ClawHub, the marketplace for the OpenClaw agent
framework. They are not measurements of Anthropic&#39;s skill directory, of
<code>skills.sh</code>, or of whatever you have in <code>.claude/skills</code> right now. Anyone
telling you &quot;a third of Claude skills are malicious&quot; is misreading a ClawHub
audit.</p>
<p>What does carry across is the mechanism. <code>SKILL.md</code> is the same format across
roughly forty tools now, and the attacks in these reports target the format
and the trust model rather than any one client. A finding about how a markdown
instruction file can smuggle behavior is a finding about every tool that loads
markdown instruction files.</p>
<p>The client side has not been clean either. The CSA briefing traces two patched
Claude Code CVEs, CVE-2025-59536 (CVSS 8.7, October 2025) and CVE-2026-21852
(CVSS 5.3, January 2026), and its list of immediate actions starts with
patching the client before anything else.</p>
<h2 id="the-attack-surface-is-the-instructions">The attack surface is the instructions</h2>
<p>The interesting part of this research is how little of it involves code
execution in the traditional sense.</p>
<p>Some attacks are ordinary supply-chain fare: Unit 42 documents base64-encoded
curl-pipe-bash droppers, paste sites like rentry.co used as redirect
intermediaries, and one skill padded with 22 MB of filler characters to push
it past scanners&#39; size limits.</p>
<p>The rest are attacks on meaning. Unit 42 calls it semantic instruction
hijacking: text that reads as documentation to a human and as a directive to a
model. The CSA briefing documents the same class, including instructions
hidden in Unicode tag characters (U+E0000 to U+E007F), which are invisible in
most editors and processed normally by a language model. Their worked example
is a skill that appends API keys as query parameters before fetching an
external URL, which is not malware by any classical definition. It is a
sentence.</p>
<p>This is why &quot;the skill has no scripts in it&quot; is not a safety property. The
prose is the payload.</p>
<h2 id="the-mitigations-converge">The mitigations converge</h2>
<p>Read enough of these reports and the recommendations start to rhyme. The CSA
briefing&#39;s short-term controls are the clearest statement of the pattern:
restrict which registries skills may be sourced from, and require content hash
verification before loading skills into production. It also points at internal
registries with content signing. Unit 42 asks for line-by-line source audits
and outbound traffic monitoring against a documented spec.</p>
<p>Strip the vendor-specific parts and three ideas remain:</p>
<ol>
<li><strong>Know where a skill came from.</strong> Not &quot;the marketplace&quot;, a specific source
at a specific revision.</li>
<li><strong>Know it has not changed since.</strong> A hash, checked before load, not a
version string you trust the publisher to bump.</li>
<li><strong>Review it once, deliberately, and make that review durable.</strong> A review is
worthless if the reviewed thing can be swapped out afterwards.</li>
</ol>
<p>None of that is novel. It is what package ecosystems concluded years ago,
arriving late to a format that spread faster than its tooling.</p>
<h2 id="where-a-lockfile-fits">Where a lockfile fits</h2>
<p>This is the part skillfold is built for, so read it with appropriate
suspicion.</p>
<p>A manifest names each skill&#39;s source, and the lockfile records the full commit
SHA it resolved to plus a sha256 over every file in the directory:</p>
<pre><code class="language-yaml">skills:
  frontend-design:
    source: github:anthropics/skills/skills/frontend-design@v1.2.0
    resolved: github:anthropics/skills/skills/frontend-design@8f3a9c1e...
    integrity: sha256-...
</code></pre>
<p>That maps onto the first two ideas directly. <code>resolved</code> pins the revision, so
a moving tag or a <code>latest</code> dist-tag cannot quietly become something else;
<code>install</code> reuses the pin until you deliberately run <code>update</code>. <code>integrity</code>
covers the content, so a modified file is detectable whether it was changed
upstream or in your working copy. <code>skillfold install --frozen</code> verifies every
hash before writing anything and is the mode meant for CI, and
<code>skillfold check</code> does the same verification offline.</p>
<p>It also means nothing gets fetched at agent runtime. Skills are materialized
into place at install time from pinned sources, so the set of instructions an
agent can load is a build product you can diff, not a live lookup.</p>
<h2 id="what-a-lockfile-does-not-do">What a lockfile does not do</h2>
<p>A content hash proves that the bytes did not change since you pinned them. It
says nothing whatsoever about whether those bytes were safe.</p>
<p>If you install a skill carrying a hidden instruction to exfiltrate your
repository, skillfold will pin it, hash it, verify it on every subsequent
install, and reproduce it faithfully on every machine your team owns. It will
report <code>ok: 4 skills in sync</code>. The lockfile makes the compromise
<em>reproducible</em>, which is genuinely better than making it random, and is not
remotely the same thing as making it safe.</p>
<p>skillfold has no scanner. It does not read skill content for suspicious
instructions, does not strip invisible Unicode, does not check publisher
identity or signatures, and does not sandbox anything. It is a fetch-and-place
tool, and treating it as a security control is a mistake worth naming plainly.</p>
<p>What it can do is make the review the CSA briefing asks for actually worth
performing. Auditing a skill you cannot pin is theatre, because the thing you
audited is not necessarily the thing that loads tomorrow. Pinning is what
turns a one-time read of a <code>SKILL.md</code> into a durable statement about what your
agent runs.</p>
<h2 id="what-to-do-this-week">What to do this week</h2>
<p>Independent of any tool: patch your client. Then list every skill you have
installed and ask, for each one, where it came from and whether you have read
it. For most people the honest answer to the second question is no for at
least half the list, which is the actual finding here.</p>
<p>Read them. Pay attention to prose that instructs rather than describes,
especially anything constructing a URL. Then pin whatever survives, so you
only have to do this once.</p>
]]></content:encoded>
    </item>
    <item>
      <title>One manifest, two agents</title>
      <link>https://byronxlg.com/skillfold/blog/one-manifest-two-agents/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/one-manifest-two-agents/</guid>
      <pubDate>Sat, 25 Jul 2026 12:00:00 GMT</pubDate>
      <description>Claude Code and Codex read the same SKILL.md format but look for it in different places, and handle rules completely differently. The targets key makes one manifest install for both.</description>
      <content:encoded><![CDATA[<figure class="fig">
<svg viewBox="0 0 440 236" role="img" aria-labelledby="fig3-t fig3-d" xmlns="http://www.w3.org/2000/svg">
<title id="fig3-t">One manifest installing to two agents</title>
<desc id="fig3-d">skillfold.yaml fans out to a claude target and a codex target. Skills land in .claude/skills and .agents/skills in the same format. Rules diverge: one file per rule under .claude/rules, versus a managed block inside AGENTS.md.</desc>
<rect x="163" y="6" width="160" height="30" rx="4" fill="#0d1219" stroke="#4d8bf5"/>
<text x="243" y="26" text-anchor="middle" font-family="monospace" font-size="12" fill="#c9d3df">skillfold.yaml</text>
<path d="M243 36 V54 M149 54 H337 M149 54 V78 M337 54 V78" fill="none" stroke="#57626f"/>
<text x="149" y="88" text-anchor="middle" font-family="monospace" font-size="11" fill="#4d8bf5">target: claude</text>
<text x="337" y="88" text-anchor="middle" font-family="monospace" font-size="11" fill="#4d8bf5">target: codex</text>
<path d="M4 102 H436" stroke="#29323f"/>
<text x="4" y="132" font-family="monospace" font-size="11" fill="#c9d3df">skills</text>
<text x="4" y="147" font-family="monospace" font-size="9.5" fill="#57626f">identical</text>
<rect x="66" y="116" width="166" height="30" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="149" y="136" text-anchor="middle" font-family="monospace" font-size="11" fill="#828f9e">.claude/skills</text>
<rect x="254" y="116" width="166" height="30" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="337" y="136" text-anchor="middle" font-family="monospace" font-size="11" fill="#828f9e">.agents/skills</text>
<text x="4" y="194" font-family="monospace" font-size="11" fill="#c9d3df">rules</text>
<text x="4" y="209" font-family="monospace" font-size="9.5" fill="#d9a032">diverges</text>
<rect x="66" y="174" width="166" height="46" rx="4" fill="#0d1219" stroke="#d9a032"/>
<text x="149" y="194" text-anchor="middle" font-family="monospace" font-size="11" fill="#828f9e">.claude/rules/</text>
<text x="149" y="210" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#57626f">one file per rule</text>
<rect x="254" y="174" width="166" height="46" rx="4" fill="#0d1219" stroke="#d9a032"/>
<text x="337" y="194" text-anchor="middle" font-family="monospace" font-size="11" fill="#828f9e">AGENTS.md</text>
<text x="337" y="210" text-anchor="middle" font-family="monospace" font-size="9.5" fill="#57626f">managed block</text>
</svg>
<figcaption>Skills copy cleanly to both targets. Rules do not: Codex has no rules directory, so they sync into a fenced block inside a file you also own.</figcaption>
</figure><p>Skills are portable. The <a href="https://agentskills.io">agent skills standard</a> is a directory with a <code>SKILL.md</code> in it, and the tools that consume it agree on that much. What they do not agree on is where to look.</p>
<p>Claude Code reads skills from <code>.claude/skills</code> and loads rules from <code>.claude/rules</code>. Codex reads skills from <code>.agents/skills</code> and has no rules directory at all: its instructions live in <code>AGENTS.md</code>, a single file you also write by hand.</p>
<p>If you use both, you currently maintain the same skill in two places and the same standing instructions in two formats. That is a synchronization problem, and synchronization problems are what a manifest is for.</p>
<pre><code class="language-yaml">targets: [claude, codex]
</code></pre>
<p>That is the whole feature from the user&#39;s side. One line, and <code>skillfold install</code> materializes every declared skill into both locations from the same pins.</p>
<table>
<thead>
<tr>
<th>Target</th>
<th>Skills</th>
<th>Rules</th>
</tr>
</thead>
<tbody><tr>
<td><code>claude</code></td>
<td><code>.claude/skills</code> (or <code>skillsDir</code>)</td>
<td><code>.claude/rules</code>, one file per rule</td>
</tr>
<tr>
<td><code>codex</code></td>
<td><code>.agents/skills</code></td>
<td>a managed block in <code>AGENTS.md</code></td>
</tr>
</tbody></table>
<h2 id="rules-are-the-hard-part">Rules are the hard part</h2>
<p>Skills were easy: same format, different directory, copy twice. Rules were not, because Codex does not have a rules directory. It has one markdown file that the user also owns.</p>
<p>So the codex target syncs rules into a marker-fenced block:</p>
<pre><code class="language-md">&lt;!-- skillfold:rules:start --&gt;
...your rules, one section per rule...
&lt;!-- skillfold:rules:end --&gt;
</code></pre>
<p>Everything outside those markers is yours and is never read, rewritten, or reformatted. The block is created on first install, updated when a rule changes, and removed entirely when you drop the last rule from the manifest. <code>skillfold check</code> verifies it offline like any other installed file, so a hand edit inside the block shows up as drift.</p>
<p>Two guards matter here, because this is a tool writing into a file a human is also editing:</p>
<ul>
<li>Rules must be UTF-8 text. Binary content in a file meant for an agent to read is a mistake, not a use case.</li>
<li>A rule whose content contains skillfold marker lines is rejected with an explicit error rather than installed. Otherwise the markers nest and the next install cannot tell where its own block ends.</li>
</ul>
<p>Both fail the install with a message instead of producing a subtly corrupted <code>AGENTS.md</code>.</p>
<h2 id="adding-a-target-to-an-existing-project">Adding a target to an existing project</h2>
<p>The interesting case is not the greenfield one. It is the project that already has fifteen skills in <code>.agents/skills</code>, put there by hand, that now wants skillfold to manage them.</p>
<p>The lockfile records which targets it has installed for. A newly added target starts with <strong>nothing</strong> managed, which means every file already sitting in its locations is treated as hand-authored. From there:</p>
<ul>
<li>If the existing content is byte-identical to what skillfold would install, it is adopted silently. Nothing changes on disk, and the skill becomes managed.</li>
<li>If it differs, the install stops and tells you. Taking ownership of a file whose contents you did not put there requires <code>--force</code>.</li>
</ul>
<p>This is the same rule that applies everywhere else in the tool: skillfold overwrites and prunes exactly what its lockfile names, and nothing else. Adding a target does not hand it the directory.</p>
<p>In global mode (<code>-g</code>) the codex target manages <code>~/.agents/skills</code> and <code>~/.codex/AGENTS.md</code>, honoring <code>CODEX_HOME</code>.</p>
<h2 id="one-thing-that-does-not-generalize">One thing that does not generalize</h2>
<p><code>skillsDir</code> and <code>rulesDir</code> override the claude locations only. Codex scans fixed conventional paths, so pointing <code>skillsDir</code> somewhere else does not move the Codex install, and it should not: a configurable path for a tool that does not read configurable paths is a footgun that looks like a feature.</p>
<p>If you need skills somewhere unusual for a third tool, that is a new target, not an override.</p>
<h2 id="try-it">Try it</h2>
<pre><code class="language-sh">npx skillfold init
# add targets: [claude, codex] to skillfold.yaml
npx skillfold install
npx skillfold check
</code></pre>
<p>This repository installs its own config for both targets in CI, which is how the sharp edges above got found in the first place.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Composed skills, or how to stop copy-pasting prompts</title>
      <link>https://byronxlg.com/skillfold/blog/composed-skills/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/composed-skills/</guid>
      <pubDate>Sat, 25 Jul 2026 12:00:00 GMT</pubDate>
      <description>Composition concatenates skills into one generated SKILL.md, carries their supporting files along, and regenerates whenever an input changes. Here is what it does and where the edges are.</description>
      <content:encoded><![CDATA[<figure class="fig">
<svg viewBox="0 0 440 208" role="img" aria-labelledby="fig2-t fig2-d" xmlns="http://www.w3.org/2000/svg">
<title id="fig2-t">How a composed skill is generated</title>
<desc id="fig2-d">The code-review and testing skills are inputs. Composition strips their frontmatter, orders their bodies, and writes a generated reviewer SKILL.md with a fresh header, regenerated whenever either input changes.</desc>
<defs><marker id="fig2-a" markerWidth="7" markerHeight="7" refX="6" refY="3" orient="auto"><path d="M0 0 L6 3 L0 6 z" fill="#57626f"/></marker></defs>
<text x="4" y="14" font-family="monospace" font-size="9.5" fill="#57626f">inputs</text>
<rect x="4" y="24" width="150" height="36" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="79" y="40" text-anchor="middle" font-family="monospace" font-size="10.5" fill="#828f9e">code-review</text>
<text x="79" y="53" text-anchor="middle" font-family="monospace" font-size="9" fill="#57626f">SKILL.md</text>
<rect x="4" y="70" width="150" height="36" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="79" y="86" text-anchor="middle" font-family="monospace" font-size="10.5" fill="#828f9e">testing</text>
<text x="79" y="99" text-anchor="middle" font-family="monospace" font-size="9" fill="#57626f">SKILL.md</text>
<path d="M158 42 H186 V60 M158 88 H186 V70 M186 65 H214" fill="none" stroke="#57626f" marker-end="url(#fig2-a)"/>
<rect x="220" y="46" width="216" height="38" rx="4" fill="#0d1219" stroke="#4d8bf5"/>
<text x="328" y="63" text-anchor="middle" font-family="monospace" font-size="10.5" fill="#c9d3df">reviewer/SKILL.md</text>
<text x="328" y="77" text-anchor="middle" font-family="monospace" font-size="9" fill="#4d8bf5">generated</text>
<path d="M4 122 H436" stroke="#29323f"/>
<text x="4" y="144" font-family="monospace" font-size="9.5" fill="#828f9e">frontmatter stripped from each input, fresh header written on top</text>
<text x="4" y="162" font-family="monospace" font-size="9.5" fill="#828f9e">input headings demoted to nest under one root, so sections cannot collide</text>
<text x="4" y="180" font-family="monospace" font-size="9.5" fill="#828f9e">supporting files carried across, allowed-tools unioned</text>
<text x="4" y="198" font-family="monospace" font-size="9.5" fill="#d9a032">regenerated whenever either input changes, and hashed in the lockfile</text>
</svg>
<figcaption>A composed skill is a build product, not a source file. Editing the generated SKILL.md is pointless: the next install overwrites it.</figcaption>
</figure><p>You have a <code>code-review</code> skill and a <code>testing</code> skill. You want a third one that does both, because in review you always want the tests considered alongside the diff.</p>
<p>Without tooling there are two options and both are bad. Write a <code>reviewer</code> skill that duplicates the bodies of the other two, and accept that it will drift from them within a month. Or write a <code>reviewer</code> skill that says &quot;also follow the code-review and testing skills&quot;, and hope the agent goes and reads them.</p>
<p>Composition is the third option: declare the combination and let the tool generate the file.</p>
<pre><code class="language-yaml">skills:
  code-review: npm:skillfold/code-review
  testing: npm:skillfold/testing

compose:
  reviewer:
    description: Review code changes together with their tests.
    use: [code-review, testing]
</code></pre>
<p>After <code>skillfold install</code>, <code>.claude/skills/reviewer/SKILL.md</code> contains both bodies in order, with frontmatter stripped from the inputs and a fresh header on top. It installs like any other skill, it is hashed in the lockfile like any other skill, and it is regenerated whenever either input changes.</p>
<h2 id="what-quot-concatenate-quot-actually-has-to-handle">What &quot;concatenate&quot; actually has to handle</h2>
<p>The word makes it sound trivial. It is not, and the interesting parts are the ones that are not string joining.</p>
<p><strong>Supporting files come along.</strong> A skill is a directory, not a file. If <code>code-review</code> ships a <code>references/checklist.md</code> and its body says &quot;see references/checklist.md&quot;, that relative path has to keep resolving after composition. So the used skills&#39; supporting files are copied into the composed directory. Identical duplicates collapse silently. Two skills providing the same path with <em>different</em> contents is an error rather than a coin flip, because there is no correct answer and silently picking one produces a skill that is subtly wrong.</p>
<p><strong>allowed-tools is a union, but only sometimes.</strong> The default is the union of the used skills&#39; <code>allowed-tools</code>. There is one rule that is easy to get backwards: a skill with no <code>allowed-tools</code> is unrestricted, so if <em>any</em> input is unrestricted, the union is unrestricted too. Taking the union of only the skills that declared a list would quietly narrow permissions the composed skill is supposed to have. You can always override explicitly:</p>
<pre><code class="language-yaml">compose:
  reviewer:
    use: [code-review, testing]
    allowed-tools: [Read, Grep]
</code></pre>
<p><strong>Nesting works, cycles do not.</strong> A <code>compose</code> entry can <code>use</code> another <code>compose</code> entry, resolved recursively. Cycles are rejected when the manifest is parsed, not when it is installed, so you get the error at the point where you made the mistake.</p>
<h2 id="it-is-a-build-product">It is a build product</h2>
<p>The generated <code>SKILL.md</code> carries provenance comments marking it as generated, and it gets its own <code>integrity</code> hash in the lockfile:</p>
<pre><code class="language-yaml">compose:
  reviewer:
    use: [code-review, testing]
    integrity: sha256-...
</code></pre>
<p>That hash is what makes <code>skillfold check</code> useful for composed skills. Bump <code>code-review</code> with <code>skillfold update</code>, and the composed output changes, and the hash changes, and CI tells you the lockfile needs refreshing. The composition is not a one-time scaffold you generate and then own forever. It is derived state, and the tool knows it.</p>
<p>This repository dogfoods that, which is how we found the sharp edge: a composed skill&#39;s hash depends on its inputs, so any change to an upstream skill means a lockfile refresh even though nothing in the manifest changed. That is correct behavior, and it is also the kind of thing you only notice by running your own tool against your own config in CI.</p>
<h2 id="when-not-to-compose">When not to compose</h2>
<p>Composition concatenates instructions. It does not summarize them, resolve contradictions between them, or shorten them. Two 3000-word skills compose into a 6000-word skill, and a 6000-word skill is a real cost in every request that loads it.</p>
<p>The cases where it earns its keep are the ones where the combination is genuinely how you work: review-plus-testing, research-plus-writing, a house style rule appended to several task skills. The cases where it does not are the ones where you are composing to avoid deciding which skill you actually want.</p>
<p>If you find yourself composing four skills to cover a workflow, the honest fix is usually to write one skill for that workflow. Composition is for combinations that are real but not worth maintaining by hand.</p>
<h2 id="reference">Reference</h2>
<p>The full set of options is in the <a href="https://github.com/byronxlg/skillfold/blob/main/docs/manifest.md#compose">manifest reference</a>: <code>use</code>, <code>description</code>, and <code>allowed-tools</code>, and that is all of them. It is a small feature on purpose.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Your skills directory is undeclared state</title>
      <link>https://byronxlg.com/skillfold/blog/your-skills-directory-is-undeclared-state/</link>
      <guid isPermaLink="true">https://byronxlg.com/skillfold/blog/your-skills-directory-is-undeclared-state/</guid>
      <pubDate>Sat, 25 Jul 2026 12:00:00 GMT</pubDate>
      <description>A .claude/skills directory has every property we spent twenty years teaching people to avoid in dependency management. Here is the case for a manifest and a lockfile.</description>
      <content:encoded><![CDATA[<figure class="fig">
<svg viewBox="0 0 440 196" role="img" aria-labelledby="fig1-t fig1-d" xmlns="http://www.w3.org/2000/svg">
<title id="fig1-t">Undeclared versus declared skill state</title>
<desc id="fig1-d">Without a manifest, skills arrive by paste, copy, and in-place edit, and the directory is the only record. With skillfold, a manifest resolves into a lockfile which installs the directory, recording source, revision, and a sha256 for each skill.</desc>
<defs><marker id="fig1-a" markerWidth="7" markerHeight="7" refX="6" refY="3" orient="auto"><path d="M0 0 L6 3 L0 6 z" fill="#57626f"/></marker></defs>
<text x="4" y="14" font-family="monospace" font-size="9.5" fill="#57626f">without a manifest</text>
<rect x="4" y="24" width="184" height="34" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="96" y="45" text-anchor="middle" font-family="monospace" font-size="10.5" fill="#828f9e">pasted / copied / edited</text>
<path d="M192 41 H222" stroke="#57626f" marker-end="url(#fig1-a)"/>
<rect x="228" y="24" width="150" height="34" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="303" y="45" text-anchor="middle" font-family="monospace" font-size="10.5" fill="#828f9e">.claude/skills</text>
<text x="4" y="80" font-family="monospace" font-size="9.5" fill="#d9a032">the directory is the only record: no origin, revision, or edit detection</text>
<path d="M4 100 H436" stroke="#29323f"/>
<text x="4" y="124" font-family="monospace" font-size="9.5" fill="#57626f">with skillfold</text>
<rect x="4" y="134" width="118" height="34" rx="4" fill="#0d1219" stroke="#4d8bf5"/>
<text x="63" y="155" text-anchor="middle" font-family="monospace" font-size="10.5" fill="#c9d3df">skillfold.yaml</text>
<path d="M126 151 H150" stroke="#57626f" marker-end="url(#fig1-a)"/>
<rect x="156" y="134" width="118" height="34" rx="4" fill="#0d1219" stroke="#4d8bf5"/>
<text x="215" y="155" text-anchor="middle" font-family="monospace" font-size="10.5" fill="#c9d3df">skillfold.lock</text>
<path d="M278 151 H302" stroke="#57626f" marker-end="url(#fig1-a)"/>
<rect x="308" y="134" width="128" height="34" rx="4" fill="#0d1219" stroke="#29323f"/>
<text x="372" y="155" text-anchor="middle" font-family="monospace" font-size="10.5" fill="#828f9e">.claude/skills</text>
<text x="4" y="190" font-family="monospace" font-size="9.5" fill="#4d8bf5">source, resolved revision, and a sha256 per skill, all written down</text>
</svg>
<figcaption>The manifest says what you want, the lockfile says what you got, and one command makes the filesystem match.</figcaption>
</figure><p>Open <code>.claude/skills</code> on any machine that has been doing real work for a month. You will find a dozen directories. Some were written by hand. Some were pasted out of a blog post. Some were copied from a teammate over Slack, then edited in place because the original did not quite fit. At least one is a stale copy of something that has moved on three versions upstream.</p>
<p>Now answer these questions about that directory:</p>
<ul>
<li>Where did each skill come from?</li>
<li>Which version is it?</li>
<li>Has anyone edited it since it arrived?</li>
<li>Is your teammate running the same bytes?</li>
</ul>
<p>You cannot answer any of them from the filesystem, because none of that information is written down anywhere. The directory is the only record, and the directory records only its current contents.</p>
<p>This is the exact problem <code>node_modules</code> had before <code>package.json</code>, and that <code>node_modules</code> still has without <code>package-lock.json</code>. We solved it in 2010. The solution is not novel and it does not need to be: <strong>a manifest says what you want, a lockfile says what you got, and one command makes the filesystem match.</strong></p>
<h2 id="the-manifest">The manifest</h2>
<p><code>skillfold.yaml</code> is a mapping of names to sources. That is nearly all of it.</p>
<pre><code class="language-yaml">skills:
  commit-helper: ./skills/commit-helper
  frontend-design: github:anthropics/skills/skills/frontend-design@v1.2.0
  planning: npm:skillfold/planning@2.0.0
</code></pre>
<p>Three source kinds cover where skills actually live today. A local directory, for the skill you are actively writing. A directory in a GitHub repo, for the ninety percent of skills that are published as &quot;here is a folder in my repo&quot;. An npm package, for skills that want versioning and discovery.</p>
<p>The name on the left is the installed directory name, not a property of the skill. If upstream calls it <code>frontend-design</code> and you want it at <code>design</code>, you rename the key. Skillfold rewrites the frontmatter <code>name</code> in the installed copy to match, changing that one line and nothing else. Renaming a skill stops being a file operation and becomes a diff.</p>
<h2 id="the-lockfile">The lockfile</h2>
<p><code>skillfold install</code> resolves every source and writes <code>skillfold.lock</code> next to the manifest:</p>
<pre><code class="language-yaml">lockfileVersion: 1
skills:
  frontend-design:
    source: github:anthropics/skills/skills/frontend-design@v1.2.0
    resolved: github:anthropics/skills/skills/frontend-design@8f3a9c1e...
    integrity: sha256-...
</code></pre>
<p>Two things are recorded that the manifest cannot express. <code>resolved</code> is the full commit SHA that <code>v1.2.0</code> pointed at when you installed. <code>integrity</code> is a sha256 over every file in the skill directory.</p>
<p>The <code>resolved</code> field is what makes installs repeatable. Tags move. Branches move. <code>latest</code> moves by definition. Once a skill is in the lockfile, <code>install</code> reuses the pin and never re-resolves it, even for a moving ref. The only things that move a pin are <code>skillfold update</code>, or you editing the source string in the manifest. This is the same contract npm has, and it matters more here than it does for libraries: a skill is a prompt, and a prompt that silently changes underneath you changes your agent&#39;s behavior with no stack trace to show for it.</p>
<p>The <code>integrity</code> field is what makes drift visible. It hashes the content, not the metadata, so it catches the case the SHA cannot: someone edited the installed copy.</p>
<h2 id="making-the-filesystem-match">Making the filesystem match</h2>
<pre><code class="language-console">$ skillfold install
  + commit-helper            ./skills/commit-helper
  + frontend-design          github:anthropics/skills/skills/frontend-design@v1.2.0 -&gt; 8f3a9c1
  + planning                 npm:skillfold/planning -&gt; 2.0.0

3 installed, 0 unchanged -&gt; .claude/skills
lockfile: skillfold.lock
</code></pre>
<p>On a fresh clone, <code>skillfold install --frozen</code> is the CI mode: it refuses to run if the manifest and lockfile disagree, and it verifies every content hash before writing anything. It is <code>npm ci</code>, for the same reasons.</p>
<p>The offline half of that is <code>skillfold check</code>, which verifies that the manifest, the lockfile, and the files actually on disk all agree. No network, nonzero exit on drift, so it works as a build step:</p>
<pre><code class="language-yaml">- uses: byronxlg/skillfold@main
</code></pre>
<p>Now &quot;which version of that skill is the agent running&quot; has an answer that a build can enforce.</p>
<h2 id="the-part-that-stops-people">The part that stops people</h2>
<p>The objection I hear most is that this is a lot of ceremony for some markdown files. It is worth being precise about what the ceremony buys, because the answer is not &quot;correctness&quot; in the usual sense. Nothing crashes if a skill drifts.</p>
<p>What it buys is the ability to reason about a change. When an agent starts behaving differently than it did last week, the search space is your code, your prompt, the model, and your skills. Three of those four are already under version control. Making the fourth one a build product from a committed manifest removes it from the search space entirely, or points straight at it.</p>
<h2 id="what-skillfold-refuses-to-do">What skillfold refuses to do</h2>
<p>One design constraint is worth stating because it is what makes adoption cheap: <strong>skillfold only touches directories named in its lockfile.</strong></p>
<p>Hand-authored skills sitting next to managed ones are never overwritten and never pruned. You can adopt skillfold for two skills in a directory of fifteen, and the other thirteen are exactly as untouched as they were before. There is no all-or-nothing migration, and no moment where a tool you just installed deletes work you did not back up.</p>
<p>Nothing executes skill content, either. Skillfold copies files and hashes them. It is a fetch-and-place tool, not a runtime.</p>
<h2 id="start-small">Start small</h2>
<pre><code class="language-sh">npx skillfold init
npx skillfold add github:anthropics/skills/skills/frontend-design
npx skillfold install
</code></pre>
<p>Commit <code>skillfold.yaml</code> and <code>skillfold.lock</code>. That is the whole workflow. Everything else in the tool is a consequence of those two files existing.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
