CREATORS
Research

From Soft to Hard Skills 

07.26.2026 | 4 min readUpdated 08.09.2026

Agent skills today are prose. A markdown file describes what the agent should do and, critically, how to invoke its tools: “run this script with bash”. All of it rides into the context window as text. The model reads it, interprets it, and improvises shell commands. Nothing is typed, nothing is verifiable before execution, and every tool description is paid again on every run. These are soft skills: suggestions the model may follow.

The fix is an old idea. MDX did it on the web years ago: one file, two readers that see different things in it. The writer sees prose; the renderer sees components. Split a skill file the same way and you get a hard skill. The prose stays soft, context the model reads. The tools become hard, typed declarations the engine executes.

The format

A hard skill is still a SKILL.md: markdown prose, plus tool code fences that declare executable tools. The grammar inside a fence is deliberately closed: an id, a description, typed params, and a run template. A malformed line is an error, not a guess.

---
skill: driver-admin
description: Administer the Driver closed beta.
includes: waitlist_admin.py
---

This skill administers the Driver closed beta:
the signup waitlist and existing users' plans.

Accepting an email lets that person register and
sends them an access email. Act on one email at a
time, and never invent an address.

```tool
id: accept_user
description: Accept a waitlisted email so it can register
params:
  email:
    type: string
    format: email
    description: the waitlisted email to grant access to
run: uv run --script waitlist_admin.py $email
```

The parser splits the file into two channels. The prose travels to the model as context, verbatim. The tool fences never reach the model as text: each one is compiled into a typed tool declaration, injected into the run’s contract, and replaced in the prose by its name. The model sees narrative plus a typed catalog; the engine sees commands.

A skill is a directory, not just a file, and includes is where it says what it carries: the script a tool runs, the folder of queries it reads. Declared, so a missing file is an error when the skill is read rather than a surprise mid-run, and so the skill can be packed and moved as one thing.

The design

Because the tool blocks parse statically, you know exactly what a hard skill can touch without executing it: the capability manifest is a property of the file, not a promise in its prose. Because arguments substitute into argv words and never pass through a shell, a hostile value cannot smuggle in commands. Because the model only ever sees the declared tools, the bounded set is the contract, and every run leaves a replayable trace. And because it is plain markdown, the file renders readably anywhere.

Two rules follow from that and are worth stating on their own. Every declared param has to appear in its run template, and every $name in a template has to be a declared param: nothing reaches a tool through an environment variable or a side channel, so the run line you read before approving a skill is the whole call. And since there is no shell, the first word of a run is the executable and can be nothing else, which means a host can tell you a skill needs uv or psql, and that you do not have it, before it runs once.

Just as important is everything this design leaves out. No repeated token spend, no project exploration, no path caching. The agent never wanders your machine looking for context: the skill is the context, and the declaration is the guardrail. The model’s freedom is exactly as wide as the tools you wrote down, and not one command wider.

We use it

Our own closed-beta operations run on this: waitlist acceptance, plan changes, credit top-ups. One hard skill over the same admin scripts we already had. Reasoning runs in Driver cloud; the scripts run locally, with local secrets that never leave the machine.

The runtime that does the local half is dar, the Driver Agents Runtime. It hands the model the tool catalog, executes what the model picks, and feeds the results back.

$ dar "grant access to [email protected]"

  driver-admin [PICKED]
  ~/ops/driver-admin/SKILL.md

  plan
    accept [email protected]

  accept_user([email protected])
    accepted

   done

  Access granted to [email protected]

A skill is registered once and runs from anywhere. Fetching a skill from a URL is the same in every way except where the text came from.

Try it

The parser and compiler behind all of this ships on npm today as @crtrs/skill. It never calls a model: you own the loop, and the package referees both directions of the contract: what the model may call and what it actually called. The whole thing is four lines:

const { parseSkillFile, compile, resolve } = require('@crtrs/skill')

const skill = parseSkillFile('SKILL.md')
const system = compile(skill)             // prose + call protocol
const output = await myInference(system)  // your model, your way
const call = resolve(skill.tools, output) // typed, validated, argv ready

Hard skills shine wherever a script should never be improvised: deploys with exact flags, admin operations granted one email at a time, the recurring ops request you keep typing by hand.

npm install @crtrs/skill

Or skip the loop entirely and run one: dar installs as a single binary and takes a key from Driver.

$ curl -fsSL https://raw.githubusercontent.com/CREATORS-INDUSTRIES/dar/main/install.sh | sh
$ dar login
$ dar add            # remember the SKILL.md here
$ dar "grant access to [email protected]"

Citation

@online{creators2026skills,
  author = {CREATORS},
  title = {From Soft to Hard Skills},
  date = {2026-07-26},
  year = {2026},
  url = {https://creators.industries/research/hard-skills},
}