Skip to content

Authoring a Skill

How to take a skill from idea to provisioned-into-a-project inside BoB. This guide is the BoB-integration layer on top of the skill-creator skill: skill-creator owns the build mechanics (scope → plan → scaffold → edit → package → iterate); this guide owns the BoB-specific wrapper around it — which surface to choose, where a finished skill lives, how it gets provisioned and promoted, and what to check before it lands.

Don’t duplicate skill-creator. When you reach the “write the SKILL.md” step, invoke /skill-creator (or read its SKILL.md) and follow it. This guide deliberately says nothing about anatomy, progressive disclosure, or degrees of freedom — those are skill-creator’s job.

1. First decide: skill, command, or agent?

Section titled “1. First decide: skill, command, or agent?”

BoB extends Claude Code through three surfaces. Picking the wrong one is the most common authoring mistake, so resolve this before you build anything.

Surface What it is Reach for it when… Lives in
Skill Auto-loaded procedural knowledge — a workflow, convention, or domain expertise Claude pulls in on its own when the description matches. The capability is knowledge or a repeatable workflow that should activate implicitly whenever the task matches, with no explicit user invocation. skills/ (universal) or registry/skills/ (provisioned)
Command A user-invoked /slash entry point — an explicit, named action. The user should deliberately trigger a discrete operation by name (and it’s fine — or better — that nothing happens unless they ask). commands/ (universal) or registry/commands/ (provisioned)
Agent A specialized Task subagent with its own context, model, and tools. The work needs isolated context or a distinct role/model — fan-out, an adversarial reviewer, a long search that shouldn’t pollute the main thread. agents/ (universal) or registry/agents/ (provisioned)

One concrete example of each:

  • Skill — timelord: whenever a doc references “today” or a relative date, Claude should automatically verify the real date. No one types /timelord; it activates on description match. → a skill.
  • Command — /start-work: the user explicitly decides “begin a new branch for this issue now.” It’s a deliberate, named action with side effects. → a command.
  • Agent — code-reviewer: a review should run in its own context with a critical role and its own model budget, returning only its verdict to the main loop. → an agent (authored via the agent-builder skill, the agent analogue of skill-creator).

Rule of thumb: implicit knowledge → skill, explicit action → command, isolated role → agent. When a feature wants both an implicit workflow and a /slash entry point, that’s two artifacts (a skill plus a thin command that invokes it), not one overloaded skill.

Author the skill with skill-creator — that is the entire build step, and this guide does not repeat it:

Slash command only — runs inside a Claude Code session (skill-creator is a skill with no terminal entrypoint):

/skill-creator

Build it in the registry (registry/skills/<name>/) unless it is genuinely universal (needed by every project — like timelord or commit), in which case it goes in skills/. When in doubt, author it in registry/ — promoting registry → universal later is cheaper than discovering a “universal” skill only one project actually uses.

A finished skill directory is just files until BoB knows about it. Registration is two moves, depending on where it lives:

Universal skills auto-load globally — no manifest, no symlink. They reach the runtime (BOB_HOME, ~/.claude/) when you deploy:

Terminal only — deploy.sh is a shell script with no slash equivalent:

Terminal window
scripts/deploy.sh --dry-run # preview the sync
scripts/deploy.sh # sync BOB_SOURCE → BOB_HOME

Registry skills are not auto-loaded — they are provisioned per project. Two steps:

  1. Sync the source into the runtime so the registry copy exists in BOB_HOME (Terminal only — deploy.sh is the shell sync mechanism, no slash equivalent):
    Terminal window
    scripts/deploy.sh
  2. Add orchestrator metadata to the skill’s frontmatter so the recommendation engine (scripts/orchestrator/recommend.js) can suggest it to the right projects:
    applies_to:
    stacks: [cloudflare-workers, d1] # stacks this skill is for
    project_types: [api, backend]
    recommended_for: [development, ops]
    category: backend-runtime
    required_with: [skills/api-designing] # optional co-requisites
    opt_in: true # only for product/vendor integrations — see below
    A skill for a specific product or vendor (Eloqua, OpenReplay, a particular SaaS) must set opt_in: true (#1980): the stack cannot tell whether a project uses that product, so the engine never auto-recommends it and it is provisioned only by an explicit bob provision add (a declared entry is kept, never a would-remove). Leave the flag off for skills the stack genuinely implies (cloudflare-dev for a Workers project). This is what lets /provision and the orchestrator answer “is this skill applicable to this project?” — see the Orchestrator Guide.

Registry skills only become active in a project that declares them. Both paths manage the same manifest — pick whichever you’re in.

Slash command:

/provision add skill <name>

Terminal: from the target project,

Terminal window
bob provision add skills/<name> # add to provisions/<project>.json + symlink it in
bob provision status # confirm it's linked (and not duplicating a global item)
bob provision refresh # reconcile all symlinks to match the manifest

The manifest at provisions/<project>.json is the single source of truth for which registry items a project loads; bob provision only ever reconciles symlinks to match it (declarative, idempotent — re-run freely).

Global-aware (#412): if the skill is already global for this machine, bob provision de-dupes it rather than double-provisioning; if a registry skill’s name collides with a global one but diverges, bob provision status reports it as a conflict with a resolution hint.

If you authored a skill inside one project and want it available to BoB globally, promote it into the source of truth rather than copying files by hand:

Terminal only — bob promote is a shell alias for promotion; there is no slash equivalent:

Terminal window
bob promote # promote a local item to the global source (BOB_SOURCE)

After bob promote lands the skill in BOB_SOURCE (in skills/ or registry/skills/), run scripts/deploy.sh to sync it to the runtime, then bob provision in any project that should load it. (bob link does the inverse — link an existing global item into a project without promoting.)

The full path, end to end:

author (registry/skills/) → deploy.sh → cdprov add (in project)
project-local skill → cdp (promote) → deploy.sh → cdprov add

Run this checklist before a skill is promoted into BoB (bob promote) or added to a manifest (bob provision add). It is the gate the promotion flow references — a skill that fails any line is not ready to land.

  • Frontmatter complete — name and a description that states what the skill does and when to use it (the description is the only thing Claude sees when deciding to auto-load — vague descriptions never fire).
  • Triggers are concrete — the description names the situations/phrases that should activate the skill, not just its topic. “Use when X, Y, or Z” beats “knowledge about X.”
  • Kebab-case naming — directory and name are lowercase kebab-case and match each other (skill-authoring, not SkillAuthoring or skill_authoring).
  • Idempotent — any bundled scripts/ are safe to re-run; the skill describes a repeatable workflow with no destructive side effects on a second invocation (the Prime Directive — see skill-creator’s IaC principle).
  • No overlap — it doesn’t duplicate an existing skill. Check skills/ and registry/skills/ first; if an existing skill is close, extend it instead of forking.
  • Orchestrator metadata (registry skills only) — applies_to / recommended_for / category set, so the recommendation engine can place it; opt_in: true if it is a product/vendor integration the stack alone cannot justify (#1980).
  • Right surface — re-confirm §1: this is genuinely implicit knowledge, not a disguised command or agent.

Every item that enters the registry is scanned by NVIDIA SkillSpector (#1328), static-only (--no-llm): deterministic, no API key, and no item content leaves the machine. The gate augments human review at promotion — it never replaces it. A clean scan does not mean the checklist above is done, and a reviewer still reads the item.

An item enters the registry one of two ways, and each is scanned:

  • bob promote runs skillspector scan <item> --no-llm before it copies anything. A risk score over SkillSpector’s threshold refuses the promotion and leaves the local file in place; CAUTION warns and promotes; a machine without SkillSpector says so and promotes, with the CI gate as the backstop.
  • A PR runs the skillspector job in CI on every changed item under skills/, commands/, agents/ or registry/. It fails only on a new finding: anything already reviewed is listed in the committed .skillspector-baseline.yaml. Until that file is committed, the job only reports. bob provision add links items that are already in the registry, so the CI gate covers it.

The engine is scripts/security/skillspector-gate.sh (check, items, scan, baseline, bootstrap); it pins SkillSpector to one commit and uv to one checksummed release. To scan an item yourself:

Terminal window
scripts/security/skillspector-gate.sh bootstrap ~/.cache/skillspector # prints the bin dir
skillspector scan registry/skills/<name> --no-llm

A new finding in your PR: fix the item, or — if the finding is a false positive or an accepted risk — add an entry to .skillspector-baseline.yaml with a reason that says why, in the same PR. BoB’s autonomous-loop skills legitimately trip the “excessive agency” rules, which is what the baseline is for. Never add a broad glob rules: entry to quiet a finding: a rule survives future edits and can hide new malicious content.

Regenerating the baseline: run the SkillSpector baseline sweep workflow (gh workflow run skillspector-baseline.yml), download its skillspector-baseline artifact, review every entry, and commit it. Do this whenever SKILLSPECTOR_REF is bumped: exact fingerprints bind the scanner version and stop matching after a bump.

Every project that linked an item keeps a symlink to it. Moving or deleting the item in BOB_SOURCE therefore needs one more step, or the next bob ready in every consuming project finds a dangling link it cannot explain (#2314):

  1. Move or delete the item — registry/<type>/ ↔ <type>/ for a demotion or promotion; delete (and tag, per the archive procedure) for a retirement.
  2. Record it in scripts/lib/retired-items.json — one entry with type, name, from (where it used to live), disposition (removed · superseded-by + replacement · moved-to-registry · moved-to-universal) and retired_by (the issue). This is required: make check-retired-items fails on any item path git history has seen that is absent without a record, and names the exact entry to add.
  3. make check — the gate is part of it.
  4. Deploy. Consuming projects converge on their next bob ready or bob provision refresh: a link to a promoted item is retargeted, one to a demoted item is retargeted when the project declares it and otherwise pruned with a bob provision add <type> <name> hint, one to a retired item is pruned naming the disposition — each action named, nothing unknown deleted. The fleet snapshot’s broken_links per project confirms it per machine.

A promotion is the mirror of §5 and takes the same record (moved-to-universal). The reference for the file, its gate and what derives from it is Retired Items.

A promotion is a move, never a copy (#2735). Leaving the old file behind creates a name that exists on both sides — and the two sides resolve differently per project: bob init links the universal copy, bob provision links the registry one, and the registry link shadows the universal one where a manifest declares it. /requirement sat like that for most of this repo’s life: the two files drifted into different commands, 15 projects ran one and 5 ran the other, and because _bob-home.farm-worker.json provisions its commands, unattended farm runs and attended laptop sessions executed different instructions under the same name. make check-item-collisions now refuses the shape — delete the old file in the same commit as the move.