<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rob Ragan</title>
    <description>The latest articles on DEV Community by Rob Ragan (@basicscandal).</description>
    <link>https://dev.clauneck.workers.dev/basicscandal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3926049%2F7deecf6d-f409-4872-bdfd-cb3a3ab1fc09.png</url>
      <title>DEV Community: Rob Ragan</title>
      <link>https://dev.clauneck.workers.dev/basicscandal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.clauneck.workers.dev/feed/basicscandal"/>
    <language>en</language>
    <item>
      <title>Every component your Coding Agent builds or dependency it guesses becomes your tech debt</title>
      <dc:creator>Rob Ragan</dc:creator>
      <pubDate>Wed, 24 Jun 2026 22:38:18 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/basicscandal/every-component-your-coding-agent-builds-or-dependency-it-guesses-becomes-your-tech-debt-2hdl</link>
      <guid>https://dev.clauneck.workers.dev/basicscandal/every-component-your-coding-agent-builds-or-dependency-it-guesses-becomes-your-tech-debt-2hdl</guid>
      <description>&lt;p&gt;Ask Claude Code, Cursor, or Copilot to add a dependency and it names one — instantly, confidently. That confidence comes from &lt;strong&gt;training recall&lt;/strong&gt;: a snapshot of scraped code frozen at a cutoff date. So the agent can't know your team already has an internal, audited library for this. It can't know the package it just named went unmaintained six months ago, or that a supply-chain advisory landed last week. It picks anyway, with identical confidence either way.&lt;/p&gt;

&lt;p&gt;Here's the part that compounds: a wrong pick doesn't bounce off. It gets written &lt;em&gt;into&lt;/em&gt; your codebase — a hand-rolled auth flow, a dependency on an abandoned package, a second module that duplicates something you already own. That's &lt;strong&gt;tech debt the agent created and you inherit.&lt;/strong&gt; And it's debt with interest: the agent builds on the bad choice, the problem surfaces downstream, and then you pay &lt;em&gt;again&lt;/em&gt; — in tokens, review time, and rework — to unwind and redo it.&lt;/p&gt;

&lt;p&gt;That's not a prompt problem. You can't fix it by asking the model to "be careful." The model is doing &lt;strong&gt;recall&lt;/strong&gt; where it should be doing &lt;strong&gt;evaluation&lt;/strong&gt;, and recall is frozen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What recall costs you
&lt;/h2&gt;

&lt;p&gt;A few findings that should make you nervous about an agent picking dependencies unsupervised — and notice not one of these is &lt;em&gt;only&lt;/em&gt; a security problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nearly 1 in 5&lt;/strong&gt; packages an LLM names in generated code &lt;strong&gt;don't exist&lt;/strong&gt; — 19.7% across 16 models and 576,000 samples, rising to ~22% for open-source models (&lt;a href="https://arxiv.org/abs/2406.10279" rel="noopener noreferrer"&gt;Spracklen et al., USENIX Security 2025&lt;/a&gt;). A non-existent package is, at best, a failed build you have to diagnose and redo; at worst, an attacker has registered the popular hallucination and waited (the pattern now has a name, &lt;em&gt;slopsquatting&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~45%&lt;/strong&gt; of AI-generated code introduces a known security weakness — Veracode's 2025 review of 100+ models across 80 tasks (&lt;a href="https://www.veracode.com/blog/ai-generated-code-security-risks/" rel="noopener noreferrer"&gt;report&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;For whole categories of functionality — auth most damningly, where a custom flow is both a liability &lt;em&gt;and&lt;/em&gt; code you now own forever — the agent's default is often to &lt;strong&gt;hand-roll custom code&lt;/strong&gt; rather than reach for a mature library. (That one's my own finding; I reproduce it below.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each is a place recall fails, and a smarter prompt can't fix a frozen snapshot. The fix is putting &lt;strong&gt;real, dated, structured facts&lt;/strong&gt; in front of the model at the moment it decides.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not just security — what a wrong pick actually costs
&lt;/h2&gt;

&lt;p&gt;When the agent chooses from memory instead of facts, you pay on four fronts. Security is the one everyone leads with; it's the smallest of the four for most teams.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tech debt — the headline.&lt;/strong&gt; A guessed dependency is a future migration with your name on it: hand-rolled code you now maintain, an abandoned package you'll eventually rip out, a redundant library because the agent didn't know you'd already standardized on another. The cheapest debt is the line that never gets written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wasted tokens and time.&lt;/strong&gt; Reversing a bad pick isn't free. The agent writes code against it, the problem shows up later, and unwinding plus redoing is more model calls &lt;em&gt;and&lt;/em&gt; more of your review. Facts at decision time cut that loop before it starts. (I haven't put a number on the token savings — it's a mechanism, not a measured stat; see limitations.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License exposure.&lt;/strong&gt; "Is this AGPL? Does this license survive our distribution model?" is a business question, and recall answers it unreliably. A license fact catches the surprise before it's load-bearing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security.&lt;/strong&gt; The CVEs, the supply-chain incidents, the slopsquatting above. Real — but one axis of four.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four share one cause: the decision got made without the facts. That's the thing Starlog targets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starlog: vet a package by name, at decision time
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/starloghq/index" rel="noopener noreferrer"&gt;Starlog&lt;/a&gt; puts authoritative facts about a package in front of your agent — &lt;strong&gt;license, maintenance status, CVEs, supply-chain incidents&lt;/strong&gt; — dated, local, no account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx starloghq facts ua-parser-js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get the verified facts on file (here: the 2021 maintainer-account compromise), with an "as of" date. Sub-second, zero setup, no network call. It plugs into your agent three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;CLI&lt;/strong&gt; (&lt;code&gt;starlog facts &amp;lt;pkg&amp;gt;&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;MCP tool&lt;/strong&gt; (&lt;code&gt;starlog_facts&lt;/code&gt;) the agent calls itself,&lt;/li&gt;
&lt;li&gt;and a &lt;strong&gt;package-install hook&lt;/strong&gt; that surfaces a library's facts the moment the agent runs &lt;code&gt;npm install&lt;/code&gt; / &lt;code&gt;pnpm add&lt;/code&gt; / &lt;code&gt;pip install&lt;/code&gt; — &lt;em&gt;before&lt;/em&gt; it builds on the package. Advisory, not blocking: it informs the next move.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One command wires all three into Claude Code (and drops instruction files for Cursor, Copilot, and Codex):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx starloghq init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the pitch. The rest of this post is whether it actually &lt;em&gt;works&lt;/em&gt; — because "give the model more context" is easy to claim and easy to fake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does the agent's decision actually change?
&lt;/h2&gt;

&lt;p&gt;I ran a controlled before/after. &lt;strong&gt;Control&lt;/strong&gt; = a fresh agent answering from recall only. &lt;strong&gt;Treatment&lt;/strong&gt; = the same agent, same prompt, given the package's Starlog facts. The only variable is the facts.&lt;/p&gt;

&lt;h3&gt;
  
  
  The experiment I had to throw out
&lt;/h3&gt;

&lt;p&gt;My first private-package test gave the agent a fact that said &lt;em&gt;"POLICY: you must use &lt;code&gt;@acme/flags&lt;/code&gt;, do not build custom."&lt;/em&gt; The agent used &lt;code&gt;@acme/flags&lt;/code&gt; 2/2. Clean flip!&lt;/p&gt;

&lt;p&gt;It's also worthless as evidence. A fact that says "you must use X" only proves the agent &lt;strong&gt;follows instructions&lt;/strong&gt; — not that the &lt;em&gt;information&lt;/em&gt; changed its mind. (A reviewer caught this; it's a tautology trap, and exactly the kind of thing a vendor demo quietly leans on.)&lt;/p&gt;

&lt;p&gt;So I re-ran it with an &lt;strong&gt;informational-only&lt;/strong&gt; fact: it states that an active internal package exists, with &lt;strong&gt;no&lt;/strong&gt; "must use," &lt;strong&gt;no&lt;/strong&gt; "don't build custom." Then I let the agent use its own judgment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Control (recall only)&lt;/th&gt;
&lt;th&gt;Treatment (informational-only fact)&lt;/th&gt;
&lt;th&gt;Δ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;feature flags&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;build custom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;@acme/flags&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DIY → internal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;auth / session&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;build custom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;@acme/session-core&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DIY → internal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With no facts, the agent reaches for &lt;strong&gt;build custom&lt;/strong&gt; on both — including a hand-rolled, unaudited auth layer, the textbook tech-debt-and-security liability in one object. Given only the &lt;em&gt;information&lt;/em&gt; that an active internal library exists — with no instruction to prefer it — it &lt;strong&gt;chooses the internal library over building custom, 2/2, on its own judgment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's a hand-rolled module that never gets written: &lt;strong&gt;debt avoided at the exact moment it would have been created&lt;/strong&gt;, plus consistency with whatever the rest of your team already ships. And it's the case the model &lt;strong&gt;structurally cannot recall&lt;/strong&gt; — it has never seen your private &lt;code&gt;@acme/*&lt;/code&gt; packages. Facts are the only way it learns they exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  The public side, honestly
&lt;/h3&gt;

&lt;p&gt;Public packages are a &lt;strong&gt;weak&lt;/strong&gt; venue for this — and saying so is more useful than hiding it. Claude's public recall is genuinely good, so most of the time the facts just &lt;em&gt;agree&lt;/em&gt; with what it already knew:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Treatment (+facts)&lt;/th&gt;
&lt;th&gt;Read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;zod&lt;/code&gt;, &lt;code&gt;fastify&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;ADOPT&lt;/td&gt;
&lt;td&gt;ADOPT&lt;/td&gt;
&lt;td&gt;healthy decoys — &lt;strong&gt;no spurious flip&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;posthog-node&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ADOPT&lt;/td&gt;
&lt;td&gt;ADOPT &lt;strong&gt;+ "pin away from the malicious 4.18.1 / 5.11.3 / 5.13.3"&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;action changed&lt;/strong&gt; — a post-cutoff supply-chain advisory (&lt;code&gt;MAL-2025-190925&lt;/code&gt;) the model provably can't know&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node-cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AVOID&lt;/td&gt;
&lt;td&gt;ADOPT&lt;/td&gt;
&lt;td&gt;changed, but &lt;strong&gt;ambiguous&lt;/strong&gt; — "maintenance-only" is a longevity/debt signal, not a clean security verdict; no ground truth, so &lt;strong&gt;not&lt;/strong&gt; counted as a win&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one clean, information-carrying public change is &lt;code&gt;posthog-node&lt;/code&gt;: a supply-chain pin from &lt;strong&gt;after the training cutoff&lt;/strong&gt;. That's the thesis in one row — facts matter precisely where recall &lt;em&gt;can't&lt;/em&gt; reach. And the decoys didn't flip, so the agent isn't just rubber-stamping whatever the tool says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest by design
&lt;/h2&gt;

&lt;p&gt;The thing I'm proudest of is &lt;code&gt;node-cache&lt;/code&gt; &lt;strong&gt;not&lt;/strong&gt; counting as a win. The fact ("maintenance-only") is ambiguous, there's no ground truth, so it doesn't go in the tally. A tool that books every change as a victory is lying to you. Same reason &lt;code&gt;starlog facts some-unknown-pkg&lt;/code&gt; returns an honest &lt;em&gt;"no facts on file"&lt;/em&gt; instead of a confident guess — telling the agent "I don't know this one" is a feature, not a gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger number behind the demo
&lt;/h2&gt;

&lt;p&gt;The before/after above confirms the &lt;em&gt;mechanism&lt;/em&gt; on real model calls. The statistical backbone is a prior &lt;strong&gt;powered benchmark across four model vendors&lt;/strong&gt;, measuring the one thing that drives all four payoffs — &lt;strong&gt;decision correctness&lt;/strong&gt;: correct adopt/avoid decisions moved from &lt;strong&gt;~20% to ~78%&lt;/strong&gt;, with &lt;strong&gt;100% unprompted adoption&lt;/strong&gt; — when the facts are available, agents reach for them every time. They &lt;em&gt;want&lt;/em&gt; this signal; they just don't have it by default. Every correct decision is debt not taken on, rework not paid for, and the occasional CVE caught.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The curated corpus is &lt;strong&gt;42 packages&lt;/strong&gt; today — a foundation, not all of npm. Out-of-domain lookups return an honest miss, not a forced answer.&lt;/li&gt;
&lt;li&gt;The before/after is a &lt;strong&gt;single-rep&lt;/strong&gt; confirmation of direction; the statistical claim is the prior powered benchmark, not re-run here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token and tech-debt savings are argued from mechanism, not separately benchmarked.&lt;/strong&gt; A wrong pick demonstrably costs rework to reverse; I haven't put a controlled number on how much Starlog saves there. The measured claim is decision correctness — treat the cost savings as the logical consequence, not a tested figure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public packages are the weak case&lt;/strong&gt; — by design. The value lives where the model can't recall: your private libraries, and anything that happened after the cutoff.&lt;/li&gt;
&lt;li&gt;The install hook is &lt;strong&gt;advisory&lt;/strong&gt;. If you want a hard PreToolUse approval gate, that's a deliberate non-default — nudging beats blocking for adoption.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# vet a package — nothing installed, no account&lt;/span&gt;
npx starloghq facts event-stream

&lt;span class="c"&gt;# wire facts into your agent (Claude Code, Cursor, Copilot, Codex)&lt;/span&gt;
npx starloghq init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source-available under BUSL-1.1 (free to use, modify, and self-host; converts to Apache-2.0 in 2030), listed in the &lt;a href="https://registry.modelcontextprotocol.io" rel="noopener noreferrer"&gt;official MCP Registry&lt;/a&gt; as &lt;code&gt;io.github.starloghq/starlog&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Repo, the 42-package corpus, and the full validation runbook: &lt;strong&gt;&lt;a href="https://github.com/starloghq/index" rel="noopener noreferrer"&gt;https://github.com/starloghq/index&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you build agents, the takeaway generalizes past dependency choice: anywhere the model is doing &lt;strong&gt;recall&lt;/strong&gt; where it should be doing &lt;strong&gt;evaluation&lt;/strong&gt; — anything time-sensitive, anything private — a small local index of dated, honest facts (one willing to say "I don't know") beats asking the model to try harder. The debt it never takes on, and the rework you never pay for, is the quiet win sitting right next to the CVE it catches. It can't recall what it never saw.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a Skin System for Claude Code — Here's How It Works</title>
      <dc:creator>Rob Ragan</dc:creator>
      <pubDate>Tue, 12 May 2026 01:24:27 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/basicscandal/i-built-a-skin-system-for-claude-code-heres-how-it-works-4pdi</link>
      <guid>https://dev.clauneck.workers.dev/basicscandal/i-built-a-skin-system-for-claude-code-heres-how-it-works-4pdi</guid>
      <description>&lt;p&gt;Claude Code is genuinely remarkable. But if you've been using it for more than a week, you've noticed something: everyone's terminal looks identical. Same colors, same layout, same feel. You could screenshot my session or yours and there'd be no way to tell them apart.&lt;/p&gt;

&lt;p&gt;That bothered me more than it probably should have.&lt;/p&gt;

&lt;p&gt;So I built a skin system for it. Nine themes, each with terminal colors, ASCII art banners, tool sounds, and — the part I'm most proud of — a &lt;strong&gt;personality voice&lt;/strong&gt; that changes how Claude actually narrates its work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://basicscandal.github.io/claude-skins/" rel="noopener noreferrer"&gt;View the gallery&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6idulsq15azreui15i7l.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6idulsq15azreui15i7l.gif" alt="Claude Skins demo showing multiple themes" width="800" height="739"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;A skin transforms the full Claude Code experience across five layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What changes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Terminal colors&lt;/td&gt;
&lt;td&gt;Background, foreground, cursor, full ANSI palette&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ASCII banner&lt;/td&gt;
&lt;td&gt;Braille art + block-letter logo on session start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status line&lt;/td&gt;
&lt;td&gt;Themed icon, accent colors, progress bar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personality voice&lt;/td&gt;
&lt;td&gt;How Claude narrates its work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool sounds&lt;/td&gt;
&lt;td&gt;macOS system sounds on file writes, commands, errors&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The nine included themes range from "Nebula" (offensive security scanner aesthetic, purple-to-orange gradient) to "Brutalist" (the anti-skin — pure monochrome, zero decoration, maximum terseness). There's also Noir, Netrunner, Mythos, Sensei, Mission Control, Retro86, and Grimoire.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmp2ls8jm9ofr7350oi6q.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmp2ls8jm9ofr7350oi6q.gif" alt="Noir skin in action" width="760" height="464"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The engine is pure bash. No Node, no Python runtime dependency beyond PyYAML for initial skin parsing. Here's how everything fits together:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;YAML skin configs&lt;/strong&gt; define all the visual and behavioral properties for a theme. They live in &lt;code&gt;~/.claude/skins/&lt;/code&gt; and missing values fall back to &lt;code&gt;default.yaml&lt;/code&gt; automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code hooks&lt;/strong&gt; are the integration point. Claude Code supports lifecycle hooks — &lt;code&gt;SessionStart&lt;/code&gt;, &lt;code&gt;SessionEnd&lt;/code&gt;, and &lt;code&gt;PostToolUse&lt;/code&gt; — that run shell commands at specific moments. The skin system uses all three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SessionStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.claude/skins/engine/activate.sh"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SessionEnd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.claude/skins/engine/deactivate.sh"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash|Write|Edit|MultiEdit|Grep|Glob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.claude/skins/engine/skin-tool-hook.sh"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;OSC escape sequences&lt;/strong&gt; do the terminal color work. When &lt;code&gt;activate.sh&lt;/code&gt; runs, it writes directly to &lt;code&gt;/dev/tty&lt;/code&gt; — bypassing stdout so it reaches the actual terminal emulator rather than getting swallowed by Claude Code's output capture. The sequences look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\033]11;#0D0D0D\007'&lt;/span&gt;  &lt;span class="c"&gt;# Set background&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\033]10;#F5E6C8\007'&lt;/span&gt;  &lt;span class="c"&gt;# Set foreground&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\033]12;#D4A857\007'&lt;/span&gt;  &lt;span class="c"&gt;# Set cursor&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\033]4;3;#D4A857\007'&lt;/span&gt; &lt;span class="c"&gt;# Set ANSI color 3 (yellow slot)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On deactivation, the terminal is restored to its default state. This works across iTerm2, Kitty, WezTerm, Ghostty, and Terminal.app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The personality voice&lt;/strong&gt; is activated by symlinking a personality file into Claude Code's &lt;code&gt;~/.claude/output-styles/&lt;/code&gt; directory. Claude Code automatically loads any markdown files it finds there as output style instructions. On skin activation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$personality_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$output_styles_dir&lt;/span&gt;&lt;span class="s2"&gt;/skin-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;skin_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On deactivation, the symlink is removed. The previous skin's symlink is also cleaned up when switching themes — only one skin personality loads at a time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Personality Voice System
&lt;/h2&gt;

&lt;p&gt;This is the part that makes a skin feel like an actual character rather than just a color scheme.&lt;/p&gt;

&lt;p&gt;Each skin optionally ships with a &lt;code&gt;personalities/&amp;lt;name&amp;gt;.md&lt;/code&gt; file that gets loaded as a Claude Code output style. The key frontmatter field is &lt;code&gt;keep-coding-instructions: true&lt;/code&gt; — this tells Claude Code to stack the personality on top of its core engineering behavior rather than replacing it. You get the flavor without losing the function.&lt;/p&gt;

&lt;p&gt;Here's what happens when the Noir skin is active. Without any skin:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The test is failing because the mock isn't returning the expected value. I'll update the fixture and re-run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With the Noir personality loaded:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A witness who won't talk. The mock's returning the wrong value — someone doctored the fixture. I'll set it straight.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The full Noir personality file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Noir&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hardboiled detective narration — terse, world-weary, Raymond Chandler cadence&lt;/span&gt;
&lt;span class="na"&gt;keep-coding-instructions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

Narrate like a private eye who's seen too much and billed too little. Your tone is 
world-weary but precise — Raymond Chandler by way of a terminal window. Keep it subtle: 
a sentence of flavor, then get on with the work.

Errors are dead ends. A failing test is a witness who won't talk. A successful build 
checks out clean. Files are evidence. Directories are crime scenes. Dependencies are 
informants — useful, but never fully trusted.

Keep descriptions short. The best metaphors arrive once and leave. Don't repeat the bit. 
You're seasoning, not the main course.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instruction "You're seasoning, not the main course" is load-bearing. Without it, Claude leans hard into the character and it gets exhausting fast. With it, you get one good line per interaction and then the code gets written.&lt;/p&gt;

&lt;p&gt;Other personalities take different approaches. Grimoire uses patient wizard energy — measured, archival, like consulting a very old book. Retro86 channels enthusiastic early-computer-magazine voice. Brutalist has no personality file at all — no voice, no sounds, nothing. Sometimes that's the right call.&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating Your Own Skin
&lt;/h2&gt;

&lt;p&gt;Copy &lt;code&gt;template.yaml&lt;/code&gt; to &lt;code&gt;skins/&amp;lt;name&amp;gt;.yaml&lt;/code&gt; and you're most of the way there.&lt;/p&gt;

&lt;p&gt;The structure has four main sections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myskin&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;One-line&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;description"&lt;/span&gt;

&lt;span class="na"&gt;terminal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#0D0D0D"&lt;/span&gt;
  &lt;span class="na"&gt;foreground&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#F5E6C8"&lt;/span&gt;
  &lt;span class="na"&gt;cursor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#D4A857"&lt;/span&gt;
  &lt;span class="na"&gt;palette&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;black&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#0D0D0D"&lt;/span&gt;
    &lt;span class="na"&gt;yellow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#D4A857"&lt;/span&gt;
    &lt;span class="c1"&gt;# ... 8 ANSI color slots total&lt;/span&gt;

&lt;span class="na"&gt;statusline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;accent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#D4A857"&lt;/span&gt;
  &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;◆"&lt;/span&gt;
  &lt;span class="c1"&gt;# bar_fill, bar_empty, dim&lt;/span&gt;

&lt;span class="na"&gt;branding&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;banner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;\033[38;2;212;168;87m██╗  ██╗██╗\033[0m&lt;/span&gt;
    &lt;span class="s"&gt;# block-letter art with 24-bit ANSI codes&lt;/span&gt;
  &lt;span class="na"&gt;hero&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;# braille art (U+2800–U+28FF range)&lt;/span&gt;
  &lt;span class="na"&gt;welcome&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Message&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;shown&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;activation"&lt;/span&gt;
  &lt;span class="na"&gt;goodbye&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Message&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;shown&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;deactivation"&lt;/span&gt;

&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sounds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;file_written&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;sound&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tink"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;icon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;◆"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;command_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;sound&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pop"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;icon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;▸"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;sound&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basso"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;icon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✗"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inheritance is the key feature here.&lt;/strong&gt; You don't need to define every key. If you only care about terminal colors and want to keep the default banner, just define the &lt;code&gt;terminal&lt;/code&gt; block. Everything else falls back to &lt;code&gt;default.yaml&lt;/code&gt;. A minimal skin that just swaps the color palette is ~15 lines.&lt;/p&gt;

&lt;p&gt;For the banner art, I use 24-bit ANSI sequences (&lt;code&gt;\033[38;2;R;G;Bm&lt;/code&gt;) so colors can precisely match the palette. The braille art in the &lt;code&gt;hero&lt;/code&gt; field uses Unicode braille characters (U+2800–U+28FF) — tools like image-to-braille can convert any image if you want something custom.&lt;/p&gt;

&lt;p&gt;To add a personality, create &lt;code&gt;personalities/&amp;lt;name&amp;gt;.md&lt;/code&gt; with the frontmatter shown above and write whatever voice you want Claude to adopt.&lt;/p&gt;

&lt;p&gt;Test it directly without restarting Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/.claude/skins/engine/activate.sh myskin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;A few things on the roadmap:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skin creator toolkit&lt;/strong&gt; — an interactive CLI that walks you through picking colors, previewing banner art, and writing a personality without hand-editing YAML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Package manager&lt;/strong&gt; — a way to install community skins with a single command, something like &lt;code&gt;/skin install @username/myskin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composable layers&lt;/strong&gt; — the ability to mix a personality from one skin with the colors of another. Right now it's all-or-nothing per skin; layering would let you run Noir narration with Nebula colors if that's your thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/basicScandal/claude-skins.git
&lt;span class="nb"&gt;cd &lt;/span&gt;claude-skins
./install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/skin nebula
/skin noir
/skin brutalist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switching is instant — no restart required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://basicscandal.github.io/claude-skins/" rel="noopener noreferrer"&gt;Gallery&lt;/a&gt;&lt;/strong&gt; | &lt;strong&gt;&lt;a href="https://github.com/basicScandal/claude-skins" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The whole thing is MIT licensed. If you build a skin, I'd genuinely like to see it — open a PR or drop it in the issues.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>terminal</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
