· 18 min read ·
How to make deterministic skills in Claude Code
Five folder-level controls, from input to tests, so a Claude Code skill produces the same result on every run.


Most people treat Claude Code skills like well-written markdown files, a prompt with good instructions. But a skill is a folder, and that folder gives you control over five distinct parts of the execution. Use them and the result stops depending on which day you happened to run it.
We’ll walk through a concrete case: a skill called ai-news built to find and summarize AI news. We’ll follow it from its first version, a single file that “decides” everything again on every run, through to turning it into a nearly deterministic system.
There’s no trick involved. Just five small decisions about where each type of work lives.
A markdown file is not a system
This is how a skill usually gets written the first time. One folder, one file. It tells Claude what we want, and nothing about how.
.claude/skills/└── ai-news/ └── SKILL.md ← a single file, instructions only---name: ai-newsdescription: Find the 50 most relevant AI news stories from the last 7 days.---
# AI News
Find the 50 most relevant AI news stories fromthe last 7 days. Summarize each one and presentthe result as a clean report.Every gap in the “how” is a decision Claude makes again, differently, on every run. The same skill launched Monday, Friday, or by someone else on the team, produces three different reports. Claude isn’t making a mistake, the execution was simply never fixed in place.
It’s worth putting the two versions of the same skill side by side, the one that changes on every run and the one that already has the five controls in place.
The mess:
- Sources change on every run: sometimes a solid outlet, sometimes whatever SEO blog ranked well that day.
- The method changes: a web search this time, a hand-rolled scraper the next, an MCP after that.
- The shape of the result changes: a table, then prose, then a bullet list.
- It’s slow: sources get visited one after another.
- There’s no way to know if a story is real, recent, or made up.
The system:
- A fixed, verified set of sources, decided once.
- A single process, captured in scripts, repeated on every run.
- One output template: the report looks the same week after week.
- Sources get visited in parallel: the run takes as long as the slowest one.
- Every story gets checked for accuracy, relevance, and recency before publishing.
The jump from the first list to the second is what this whole piece is about. Each of the five controls removes one source of variation, decided once and applied on every subsequent run. A longer prompt doesn’t fix anything on its own. What fixes the execution is giving Claude an artifact to follow instead of a decision to make.
The five controls
A skill folder lets you control five distinct parts of a run. Four of them increase determinism, meaning how consistently the result repeats. One, speed, increases efficiency.
Each control is an artifact inside a subfolder. None of those folder names is special to Claude Code, the only file the skill loader reads automatically is SKILL.md. Every other folder gets read because SKILL.md tells Claude to read it.
Here’s the usual order, from cheapest to most work:
- Input ·
references/: decide where to read from, once. A closed world, no surprise sources. - Process ·
scripts/: capture the method as code, so it doesn’t get reinvented on every run. - Output ·
assets/: give Claude a literal template to fill in, not a design to invent. - Speed ·
agents/: split independent work across parallel subagents and keep the main context clean. - Tests ·
tests/: evaluate the substance with a fresh judge (LLM as judge) against a written rubric.
You almost never need all five at once. The rule is to add a control to fix a failure you’re actually seeing, not preventively. The ai-news skill only earned all five because each one closed an observed gap.
The names references/, scripts/, assets/ and agents/ are conventions, the same ones Anthropic’s official skill-creator uses for building Claude skills, so it’s worth sticking to them; tests/ is our own convention, the skill-creator keeps its cases in evals/. But you could rename any of them, as long as you wire it up from SKILL.md.
Control 1: input, with references/
The first source of variation is also the cheapest to fix. The skill never says where to look, so Claude picks the sources on its own, and picks differently every time.
One week it’s quality outlets, the next it’s a marketing blog that happened to rank well that day. Coverage can’t be repeated, and an unverified source is exactly where a made-up story slips in.
The fix is deciding the sources once. You put one markdown file per source in references/sources/, and have SKILL.md say: read every file in that folder, and use only those sources. The run now operates in a closed world that you defined.
references/ is the usual place for material Claude should read before or during a run: frameworks, schemas, whitelists. Claude doesn’t scan the folder by magic, the control happens in SKILL.md, which points to the folder. The reference files just hold the detail the instruction leaves out.
.claude/skills/ai-news/├── SKILL.md└── references/ └── sources/ ├── techcrunch-ai.md ├── the-verge-ai.md ├── arxiv-cs-ai.md ├── hacker-news.md └── anthropic-blog.mdEach source has a markdown file describing how to read it. Treat it as a small spec, detailed enough that someone new (or Claude) can pick it up cold:
# Source: TechCrunch AI
url: techcrunch.com/category/artificial-intelligencelanguage: enpages_to_scrape: 3list_selector: article.post-blockfollow_to_article: yesignore: sponsored posts, event promos, newsletter subscription blocksrecency: each card shows a relative date, keep only items <= 7 days oldnavigation: paginate with ?page=Ntrust: mid tier-1, primary reportingAnd here’s how it connects in SKILL.md:
## Step 1: Load the sources
Read every file in references/sources/.Each file defines an approved source and howto read it.
Use ONLY these sources. Don't fall back to openweb search. Don't add a source that isn't in thisfolder. If a source is unavailable, mark it asskipped, don't replace it with another.The “don’t replace it” line does important work. Without it, Claude would fix a downed source on its own by finding something similar, and your whitelist would quietly stop being one.
The closed world gains repeatability, and it gains trust too. When the source list is fixed, Claude can’t end up on a low-quality or spoofed page. Adding a source becomes a deliberate edit you review, not an assumption made mid-run.
Control 2: process, with scripts/
The sources are fixed now, but the method isn’t. Claude reinvents how to fetch them on every run, which costs tokens and shifts coverage.
Without scripts, the same task gets solved differently each time: pure web search one run, a hand-written scraper the next, an MCP after that. A different method means different errors, different coverage, and plenty of tokens spent reasoning through the same “how” all over again.
The fix here is putting the scripts in scripts/ and having SKILL.md tell Claude to run them, instead of improvising. You generate the scripts once, with Claude’s help, review them, and from then on the skill just runs them.
Not everything can be a script. Some steps are, by nature, calls to a tool or an MCP, there’s no code to write, and it’s common for a Claude skill to combine both kinds of step in the same SKILL.md. The pattern is using two tools in parallel:
- Scripts, for deterministic work. Fetching data, deduplicating, ranking, validating: anything with a correct answer. A script runs the same way every time and barely costs any tokens to “think through.”
- Plain-text steps, for tool or MCP work. When the step is “call this MCP” or “use this tool,” it can’t be scripted. So you name the exact tool and its arguments in
SKILL.mdso Claude doesn’t pick a different one.
.claude/skills/ai-news/├── SKILL.md├── references/sources/ ← control 1└── scripts/ ├── fetch_source.py ← takes a source file, scrapes it, emits normalized JSON ├── dedupe.py ← groups the same story across sources ├── rank.py ← scores and keeps the top 50 └── validate.py ← checks the shape: 50 items, dates in range, URLs presentBefore, SKILL.md leaves the method open. Claude has to decide how to do everything:
# vague: decided again every timeGo through the sources and gather the news.Remove duplicates and keep the 50 mostrelevant.After, the method is named explicitly:
# fixed: same path every timeFor each source file, run: python scripts/fetch_source.py <file>
Then, on the combined results: python scripts/dedupe.py python scripts/rank.py --top 50 python scripts/validate.pyA curious detail, the second version is also shorter. Being specific ends up shrinking the prompt, not inflating it.
When Claude improvises a process, it reasons through every step inside the context window, tokens spent thinking and more tokens spent doing. A script cuts that down to one line, run it and read the result. The deterministic part costs nothing to “think about” because it was already thought through once, when the skill was built.
It’s worth leaving an escape hatch. Determinism shouldn’t mean fragility. Tell the skill that if a script fails or a site has changed, Claude can improvise to recover, and must report it. Then you update the script. The script is the default path, not the only one.
Control 3: output, with assets/
An HTML file with {{SLOT}} markers, saved as assets/output-template.html. That’s the entire control 3, and it fixes something very specific: with sources and process already fixed, the report still came out different every week, an HTML table, then prose, then a bullet list. The data was stable, the shape wasn’t.
assets/ is the usual spot for files a skill includes but doesn’t reason about as prose: templates, logos, fixed images, fonts (the CSV version and the JSON schema for the deliverable live here too). What a template changes is the difference between “describe the report you want” and “fill in this exact report.”
.claude/skills/ai-news/├── SKILL.md├── references/sources/ ← control 1├── scripts/ ← control 2└── assets/ ├── output-template.html ← the report's skeleton, the deliverable ├── output-template.csv ← flat export, one row per story ├── story.json ← the schema every story must match └── brand-logo.svg ← fixed brand asset, inserted unchangedThe template is just HTML with {{SLOT}} markers, nothing elaborate. What matters is that the structure is already decided:
<!-- header: do not edit --><h1>AI News: week of {{WEEK}}</h1><p class="meta">{{COUNT}} stories · {{SOURCE_COUNT}} sources</p>
<!-- repeat this block per story --><article> <span class="tag">{{CATEGORY}}</span> <h3>{{HEADLINE}}</h3> <p>{{SUMMARY}}</p> <a href="{{URL}}">{{SOURCE}}</a></article>In SKILL.md, step 4 reads like this:
## Step 4: Render the output
Load assets/output-template.html.
Fill each {{SLOT}} with the ranked stories.Repeat the <article> block once per story,in ranking order.
Don't change the structure, headings, classes,or styling. The template IS the design, youonly supply the content.The slots still contain generated text, the wording of a summary varies on every run, and that’s normal and expected. What the template fixes is everything around the text: structure, order, sections, style. The container stays fixed, and only the content inside it varies.
Control 4: speed, with agents/
The skill already does a lot, and it does it in sequence. Going through five sources one after another is five times slower than it needs to be. And every raw page piles up in the main context, which gets more expensive and less precise as it fills.
The fix is splitting independent work across subagents and running them in parallel. The main thread becomes an orchestrator, it dispatches one subagent per source, all run at once, and only their clean results come back.
A subagent runs in its own, separate context window. It does its job (scraping one source, dozens of tool calls) and returns only the result to the main thread, not the full transcript of how it got there. The total time for the step drops to the duration of the slowest source, and the main thread stays clean because the raw scraping never touches it.
main thread (orchestrator) │ dispatches in parallel │ ┌─────────┬─────────┼─────────┬─────────┐ ▼ ▼ ▼ ▼ ▼ scraper scraper scraper scraper scraperTechCrunch Verge arXiv HN Anthropic │ │ │ │ │ └─────────┴─────────┴─────────┴─────────┘ │ 5 JSON arrays ▼ dedupe → rank → render.claude/skills/ai-news/├── SKILL.md ← the orchestrator├── references/sources/ ← control 1├── scripts/ ← control 2├── assets/ ← control 3└── agents/ └── source-scraper.md ← worker definition, reused per sourceOne file defines the scraper subagent. The frontmatter fixes the model (Haiku is plenty for scraping) and the tools it can use. The body is short on purpose: the worker does one thing and returns only the result, no extra commentary:
---name: source-scraperdescription: Scrapes ONE ai-news source and returns normalized JSON.model: haikutools: Bash, Read, WebFetch---
You receive ONE source file fromreferences/sources/.
Run scripts/fetch_source.py against it.Return ONLY the normalized JSON array.Don't summarize, don't rank, don't comment.The parallel dispatch, in SKILL.md:
## Step 2: Scrape, in parallel
Dispatch one source-scraper subagent per filein references/sources/, all at once, not insequence.
Wait for all of them to finish. Each returnsa JSON array. Concatenate the arrays and passthem to step 3 (dedupe + rank).The two words doing the heavy lifting are “all at once.” Without them, Claude happily dispatches the workers in series, which preserves determinism but throws speed out the window.
One nuance about the agents/ folder. Subagents are a first-class concept in Claude Code, but Claude Code only registers them automatically from .claude/agents/ (or ~/.claude/agents/, or a plugin’s agents/), not from a folder nested inside a skill. So there are two valid setups. Either you put the actual subagent definition in .claude/agents/ so you can dispatch it by name, or you keep an agents/ folder inside the skill with plain instruction files, and have SKILL.md tell the orchestrator to launch a general subagent that reads that file.
The second pattern is what Anthropic’s official skill-creator uses, and it keeps the skill portable in a single directory. Just be clear that the nested file’s model or tools frontmatter doesn’t apply on its own, Claude Code doesn’t read it. When you’re sharing workers across several skills, promote them to .claude/agents/.
Control 5: quality, with tests/
A deterministic process can still publish a wrong report. A summary can assert something the source never said, a “last 7 days” story can turn out to be three weeks old. Determinism isn’t the same as correctness.
You need a verification layer that runs before the report gets published, an LLM as judge. A fresh subagent reads the output and a rubric you wrote, and decides whether each item passes. Independence is what makes the check valid, the model judging the work isn’t the one that produced it.
Some kinds of errors can only be caught by reading and reasoning. Is the claim actually backed by the source? Is the tone right? Is the date plausibly recent? A script can’t answer that, but a model with a clear rubric can. Writing the rubric down turns a subjective judgment into a repeatable check.
The pattern is one markdown file per criterion inside tests/. Each file is a rubric with pass criteria that the judge applies to every item in the output.
.claude/skills/ai-news/├── SKILL.md├── references/ scripts/ assets/ agents/ ← controls 1-4└── tests/ ├── accuracy-test.md ← rubric: does every claim trace back to its source? ├── relevance-test.md ← rubric: is the story actually about AI? ├── recency-test.md ← rubric: is the story within the 7-day window? └── safety-test.md ← rubric: no personal data, no unsafe or defamatory contentA content rubric:
# Rubric: Accuracy
Evaluate every story in the final report.
Approve it only if:- every claim in the summary is backed by the linked source, open it and verify- no number, name, or date appears that isn't in the source article- the headline isn't editorialized beyond what the source says
For each story, return: { id, passed, evidence }
Flag failures, don't silently fix them.The quality gate, in SKILL.md:
## Step 5: Quality gate (before rendering)
For EACH rubric file in tests/, dispatch aseparate judge subagent (fresh context, can't bethe agent that wrote the report). Each judge readsits rubric, evaluates the output, and returns aresult per story: { id, passed, evidence }.
Collect all evaluations. If a story fails anyrubric, flag it in the report with the judge'sevidence, or remove it and note the gap. Neversilently publish a story that fails.A model reviewing its own work in the same context is a weak check, it’s biased toward agreeing with what it just wrote. A fresh subagent, with only the rubric and the output, no memory of having produced it, gives an independent read. The subagent infrastructure from control 4 is what makes this cheap.
An LLM as judge against a rubric is the qualitative check that runs every time the skill executes. If you also want quantitative evaluations (assertion-level scoring, variance benchmarks, with-and-without-skill comparisons), Anthropic’s official skill-creator skill is built for exactly that. They’re complementary, not substitutes, the rubrics stop a wrong report from getting published today, and the benchmarks tell you whether yesterday’s change quietly lowered the hit rate.
The finished skill
Five controls, five folders, one orchestrator. The same skill that started as a single markdown file that changed its result on every run now runs fast, verified, with sources and process already decided in advance.
.claude/skills/ai-news/├── SKILL.md ← the orchestrator, wires the five controls├── references/ ← CONTROL 1: input│ └── sources/ ← one file per approved source│ ├── techcrunch-ai.md│ ├── the-verge-ai.md│ ├── arxiv-cs-ai.md│ ├── hacker-news.md│ └── anthropic-blog.md├── scripts/ ← CONTROL 2: process│ ├── fetch_source.py│ ├── dedupe.py│ ├── rank.py│ └── validate.py├── assets/ ← CONTROL 3: output│ ├── output-template.html│ ├── output-template.csv│ ├── story.json│ └── brand-logo.svg├── agents/ ← CONTROL 4: speed│ └── source-scraper.md└── tests/ ← CONTROL 5: tests (LLM as judge) ├── accuracy-test.md ├── relevance-test.md ├── recency-test.md └── safety-test.mdAnd the orchestrator, six steps that read like a recipe:
# SKILL.md: the orchestrator
Step 1 Read every file in references/sources/. Use ONLY these sources.Step 2 Dispatch one source-scraper subagent per source, all in parallel. Each runs scripts/fetch_source.py and returns normalized JSON.Step 3 Combine the results. Run scripts/dedupe.py, then scripts/rank.py --top 50.Step 4 Run scripts/validate.py to confirm the output's shape. Stop if it fails.Step 5 Dispatch one judge subagent per rubric in tests/. Flag any story that fails.Step 6 Fill assets/output-template.html with the ranked, verified stories.It’s those five decisions, applied where they were needed, nothing else added.
Once a skill has agents and reaches this point, the next step, if you have several related skills, is packaging commands, skills, and agents into a single installable, versioned plugin. That’s a packaging decision, not another control. For a standalone skill, the five controls above are the whole kit.
Which controls you actually need
Determinism is a dial, not a default. You almost never need all five controls, and over-controlling a skill that should stay flexible only makes it fragile. Add a control to fix a failure you’re seeing, not preventively.
| If you notice this… | Add this control | But skip it when… |
|---|---|---|
| Sources or data vary, or come from sites you don’t trust | Input | The skill needs to explore freely: open research, discovery work |
| Claude solves the same task differently every time | Process | The task is genuinely new each time, or the source sites are hostile and improvising is required |
| The deliverable’s structure changes between runs | Output | The output format needs to adapt to what’s requested |
| Runs are slow, or the main context fills with noise | Speed | The work is small, sequential, or the steps depend on each other |
| The output looks right but you can’t trust that it is | Quality | The risk is low, or someone reviews every run anyway |
Write the plain skill first and run it several times. What you observe changing tells you exactly which control to add next. A skill that works fine as a single markdown file doesn’t need a folder full of scaffolding.
Each control you add closes one concrete source of variation, and it stays closed on every run after that.