<?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.github.io/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>Mon, 27 Jul 2026 12:00:00 GMT</lastBuildDate>
    <atom:link href="https://byronxlg.github.io/skillfold/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>A valid provenance attestation shipped malware. Here is how.</title>
      <link>https://byronxlg.github.io/skillfold/blog/asyncapi-npm-provenance/</link>
      <guid isPermaLink="true">https://byronxlg.github.io/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.github.io/skillfold/blog/agent-skill-supply-chain-study/</link>
      <guid isPermaLink="true">https://byronxlg.github.io/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.github.io/skillfold/blog/microsoft-agent-skills-dotnet-python/</link>
      <guid isPermaLink="true">https://byronxlg.github.io/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.github.io/skillfold/blog/mcp-stateless-rewrite/</link>
      <guid isPermaLink="true">https://byronxlg.github.io/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.github.io/skillfold/blog/what-the-skill-supply-chain-research-recommends/</link>
      <guid isPermaLink="true">https://byronxlg.github.io/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.github.io/skillfold/blog/one-manifest-two-agents/</link>
      <guid isPermaLink="true">https://byronxlg.github.io/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.github.io/skillfold/blog/composed-skills/</link>
      <guid isPermaLink="true">https://byronxlg.github.io/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.github.io/skillfold/blog/your-skills-directory-is-undeclared-state/</link>
      <guid isPermaLink="true">https://byronxlg.github.io/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>
