Skip to content
Back to skills

Agent Skills

ASecurity

How to use @owlmeans/agent-skills — the CLI that installs embedded package guidance into a project, and the ./llm prompt plugins that load @owlmeans package skills and the project's own installed skills into a system prompt. Auto-invoked when importing owlmeansPackagesPlugin, projectSkillsPlugin, projectSkillsAgentPlugin, parseSkillFile, or running the installer.

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 22, 2026
devopstypescriptrustgoshellnodeexpressgitapi

Works with

  • claude code
  • cli
  • api

Security analysis

A100/100

Scanned October 6, 2026

npx -y skills add owlmeans/common --skill agent-skills --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Agent Skills?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Agent Skills
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/owlmeans-agent-skills/badge)](https://www.skillsdirectory.com/skills/owlmeans-agent-skills)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: agent-skills
description: How to use @owlmeans/agent-skills — the CLI that installs embedded package guidance into a project, and the ./llm prompt plugins that load @owlmeans package skills and the project's own installed skills into a system prompt. Auto-invoked when importing owlmeansPackagesPlugin, projectSkillsPlugin, projectSkillsAgentPlugin, parseSkillFile, or running the installer.
user-invocable: false
---

# @owlmeans/agent-skills

**Layer:** Cross-cutting domain
**Install:** `"@owlmeans/agent-skills": "^0.1.18-rc.51"` — in `devDependencies` for the CLI, in
`dependencies` for the `./llm` plugins (plus the `@owlmeans/llm*`, `@owlmeans/agent` and
`@langchain/core` **optional peers**)

Two halves that never meet at runtime:

- the package root — the **installer CLI**, which lifts embedded `agent-meta/` guidance out of
  installed `@owlmeans/*` packages into `.agents/skills/`. It has **no runtime dependencies**
  and must keep it that way.
- `./llm` — **prompt plugins** that put guidance into a system prompt. Everything they import
  is an optional peer, which is why they are a separate subpath and are never re-exported from
  the package index.

## Key exports (`@owlmeans/agent-skills/llm`)

| Export | Description |
|---|---|
| `owlmeansPackagesPlugin(options?)` | Order 50. Detects `@owlmeans/*` mentions in the messages, loads those packages' published skills into `PromptBlock.Packages`. |
| `projectSkillsPlugin(options?)` | Order 55. Indexes the project's own installed skills into `PromptBlock.Skills`, and activated bodies into `PromptBlock.Packages`. |
| `projectSkillsAgentPlugin(options?)` | An `AgentPlugin` exposing the `read_skill(name)` tool, so a running agent pulls a body when it turns out to need one. |
| `loadProjectSkills(provider, options?)` | Every valid skill in a project, sorted by name. |
| `parseSkillFile(path, content)` · `parseSkillFrontmatter(content)` | The spec parser/validator. Returns `null` for anything that is not a valid skill. |
| `matchRules(rules, signals)` · `pickByModel(index, signals, model, max)` | The two activation mechanisms, usable standalone. |
| `projectSkillsCache(provider)` · `invalidateProjectSkills(key?)` | The per-project read cache and its invalidation. |
| `loadPackageSkills`, `manifestHelper.{stripMeta, parseManifest, skillEntries, toSkill, unscoped}` | Embedded-manifest primitives. |

## Key exports (package root — the installer as a library)

The CLI is a thin shell over these, so a build script can drive an install without spawning `npx`.

| Export | Description |
|---|---|
| `run(args)` | The whole flow: refuse-if-linked → discover → plan → print → apply. Returns `{ code }`, never calls `process.exit`. |
| `discover(dir, { extras?, only? })` | Every installable entry found in the tree, deduped by name. |
| `detectLinked(dir)` | `{ linked, evidence }` — the exit-4 check. |
| `planInstall(entries, dir, { force? })` · `AUTO_GENERATED_BANNER` | Entry → `install` / `update` / `skip-uptodate` / `conflict`, decided by the banner and a byte comparison. |
| `applyInstall(items, dir)` | Writes the plan and maintains the Claude Code symlinks. Returns the counts. |

## The Agent Skills standard

A skill is a **directory** whose `SKILL.md` opens with YAML frontmatter
([agentskills.io](https://agentskills.io)):

| Field | Rule |
|---|---|
| `name` | required, 1-64 chars, `[a-z0-9-]`, **must equal the directory name** |
| `description` | required, 1-1024 chars |
| `license` · `compatibility` · `metadata` · `allowed-tools` | optional |

**Progressive disclosure is the point.** `name` + `description` are cheap and always loaded;
the body loads only on activation. Everything below is that principle expressed as a cache
layout.

Parsing rules this package holds to:

- The frontmatter parser is **hand-rolled** — flat `key: value`, one nesting level, folded
  continuation lines. No YAML dependency: the CLI half ships with none.
- An invalid file is **skipped silently, never thrown**. A skills directory holds drafts and
  notes, and none of them may take down a model call.
- A `name` that differs from its directory is invalid — the name is how everything else
  addresses the skill, so the mismatch makes it unreachable.
- Bodies are stripped of frontmatter and of any `AUTO-GENERATED` banner (`manifestHelper.stripMeta`).

## `owlmeansPackagesPlugin` — what a named package contributes

A package that was named gets everything it documents: within a package there is no relevance
ranking, by design. Two ceilings bound the block anyway, and both matter when a prompt looks
larger than expected:

- `categories` (default `['package-specific', 'multi-package']`) — manifest categories to load.
  `general` entries say how an agent should behave rather than what a package does, so they are
  the host's own catalogue's job and are excluded unless asked for.
- `maxPackages` (default `5`) — mentioned packages loaded into one prompt. A message that
  name-drops a dozen packages is rarely asking about all of them, and each one costs context.

Resolution per package: the host's `LlmFileProvider` → an installed copy under `node_modules` →
the canonical repository over HTTPS — or, when `localRoot` names a checkout of it, that checkout
from disk INSTEAD (the same embedded `agent-meta/` copies at the same repo-relative paths, source
`checkout`; GitHub is then
never asked, and `fetch` does not apply). Every failure is a miss, never a throw, and misses are
cached too.

| Option | Default | Meaning |
|---|---|---|
| `files` | — | Host file access, tried first. |
| `scopes` | `['@owlmeans']` | Package scopes detected in the messages. |
| `dir` | `process.cwd()` | Where the local `node_modules` walk starts, going upward. |
| `exclude` | `[]` | Packages the static `Skills` block already carries. |
| `categories` | `['package-specific', 'multi-package']` | Manifest categories to load. |
| `maxPackages` | `5` | Mentioned packages loaded into one prompt. |
| `repo` · `ref` | `owlmeans/common` · `main` | The canonical fallback and the ref read from it. |
| `localRoot` | — | A checkout of `repo` read from disk in place of that fallback (development hosts). |
| `fetch` · `timeout` | `true` · `5000` | Whether the network fallback runs at all, and its deadline. |

## `projectSkillsPlugin` — the two halves, two blocks

```typescript
// `files` is the host's own LlmFileProvider, as the value or a thunk for one.
ctx.prompts().use(projectSkillsPlugin({
  files: () => fileProvider,                   // fallback; the compose context's wins
  rules: [
    { skills: ['deploy'], when: { paths: ['charts/**', '*.yaml'] } },
    { skills: ['auth-protocol'], when: { purposeType: ['coder'], mention: ['token', 'guard'] } },
  ],
}))
```

| Half | Block | Lifetime | Content |
|---|---|---|---|
| index | `Skills` | per project | `- <name> — <description>` per skill, sorted, capped |
| body | `Packages` | per request | `promptRenderHelper.renderSkill()` of each activated skill |

The index MUST stay byte-stable for a project: it sits behind a cache breakpoint that every
call about that project shares. That is why entries sort by `promptRenderHelper.compareAlias` (code units, never
`localeCompare`), descriptions are clipped to a fixed `descriptionChars`, and the heading and
lead line are constants.

| Option | Default | Meaning |
|---|---|---|
| `files` | — | Fallback provider; the compose context's own wins. |
| `dir` | `.agents/skills` | Where the skills live, relative to the project root. |
| `rules` · `activate` | `[]` · — | The deterministic rules, and the host's own decision on top. |
| `exclude` | `[]` | Names neither indexed nor activated. |
| `maxActivated` | `3` | Bodies loaded into one prompt. |
| `maxIndexEntries` · `descriptionChars` | `40` · `160` | Index size, and the clip that keeps each line stable. |
| `maxBodyChars` | `24000` | Length one activated body is clipped to. |
| `relevanceModel` | `false` | Spend one cheap-model call on the pick. |
| `listTtlMs` | `30000` | How long a directory listing is trusted. |

**Reads go through `LlmFileProvider` only — never `node:fs`.** The project an agent works on
is routinely a sandbox, a container or a remote workspace; a plugin that reaches for the local
filesystem describes the wrong project instead of failing.

### Activation — what turns an index line into a body

In precedence order, capped at `maxActivated` (3), then sorted for byte-stability:

1. **Named by the call** — a name in `PromptInput.skills` / `callSkills` that matches an
   installed skill. A caller that named it has stated the intent the rest is inferring.
2. **Deterministic rules** — `{ skills, when: { purposeType?, action?, mention?, paths? } }`.
   Every predicate present must hold. An empty `when` never fires: a skill that activates on
   every request is the static `Skills` block written in the wrong place.
3. **`activate(signals)`** — the host's own decision.
4. **`relevanceModel: true`** — ONE cheap `ctx.utility()` call, answering with names taken
   from the index. **Off by default.** Memoised per (project key + a hash of the signals) so a
   retry re-sends identical bytes.

Only 1-3 run by default, and only they may ever influence a cached block.

### Dedupe and exclusion

- Every body emitter claims `skill:<name>` (`ctx.claim`) first. `owlmeansPackagesPlugin` runs
  at 50 and claims first **by design** — a request that names `@owlmeans/auth` is a more
  specific signal than a rule, so the package's copy wins the tie and the project's is skipped.
- A name that resolves in the host registry (`ctx.resolve([name])`) is dropped from the index
  entirely: `skillsPlugin` already renders it, and indexing it too advertises one thing under
  two descriptions.

### Caching

`projectSkillsCache` is module-level and keyed by `LlmFileProvider.key`, falling back to a
`WeakMap` on the provider INSTANCE when it declares none — two keyless providers may be two
different projects, and serving one's files to the other is worse than any miss. Listings
expire on `listTtlMs` (30s, because skills are edited by hand mid-run); parsed bodies are kept
per path. After writing a skill file into a project an agent is still working in, call
`invalidateProjectSkills(key)`.

## `projectSkillsAgentPlugin` — disclosure inside the loop

```typescript
makeAgentModel({ exec, tools, plugins: [projectSkillsAgentPlugin({ files: () => fileProvider })] })
```

Composition happens once, before the first token, when nobody yet knows which of twelve turns
will touch the deployment guidance. `read_skill(name)` is how a running agent pulls a body at
the moment it needs one. Without a `files` provider the plugin contributes no tool at all. The
tool never throws — a rejected tool call aborts the whole LangGraph superstep.

## Installer CLI

`npx @owlmeans/agent-skills@^0.1.18-rc.51` walks the **whole** project tree, reads every nested
`<dir>/node_modules/@owlmeans` scope it finds, and copies each `agent-meta/` skill into
`.agents/skills/<name>/SKILL.md`. A workspace keeps its dependencies beside the workspace member
that declares them, so the root scope is routinely empty and a root-only scan would find nothing.
The walk never descends into a `node_modules` directory, skips hidden directories, and reads each
physical package once however many places it is linked from.

Only `kind: 'skill'` entries install. A package published before manifest schema v2 also carries a
`kind: 'instruction'` twin of the same knowledge — the Copilot format that predates the Agent Skills
standard — and those are dropped, so a mixed `node_modules` never writes both halves.

Two rules the discovery half must keep:

- **Dedup compares prereleases.** Every version on the current line is an `-rc.N`, so a comparison
  that stopped at major/minor/patch would call every contest a tie and keep whichever copy the
  walk reached first. Precedence is semver's: a release outranks any prerelease of the same
  version, identifiers compare field by field, numeric ones numerically and below alphanumeric.
- **`--only` filters before dedup, not after.** Two packages may ship a same-named skill; judging
  the filter against whichever copy won the dedup drops a skill the named package really ships.

An unresolved conflict costs one file, never the run: the clean skills are written first, then the
conflicts are reported, and only then does a non-interactive run without `--yes` or `--force`
return exit 5. A conflicted skill is still symlinked into `.claude/skills/` — it exists on disk,
and Claude Code has to see it. Exit codes: 2 argument parse, 3 nothing found, 4 linked monorepo,
5 unresolved conflicts, 1 fatal.

It refuses (exit 4) in a **linked** monorepo, where the root `.agents/skills/` is already
canonical and `sh .agents/scripts/link-skills.sh` — run by the committed `SessionStart` hook —
already puts it in front of every agent, so installing would only add stale copies. That check
scans the ROOT scope alone, on purpose: a dev-linked monorepo hoists escaping symlinks there, while
a scaffolded workspace app keeps its dependencies nested and must install normally.

Flags: `--dir` · `--yes` · `--only <pkg,...>` · `--extras` / `--no-extras` · `--force` ·
`--dry-run` · `--help`. `--claude-only` and `--copilot-only` are accepted and do nothing — one
skill store serves every agent. Full flag and conflict reference: package `README.md`.

### Extras — the guidance that belongs to no package

Some skills describe how an agent should work in an OwlMeans project at all rather than what a
package does. A skill becomes one by declaring

```yaml
metadata:
  scope: general
```

in its canonical frontmatter; everything so marked is embedded into THIS installer's own
`agent-meta/`, alongside this package's own skill. Extras install by default even when no other
`@owlmeans/*` package is present, which is how a freshly scaffolded project gets harness guidance
before its first dependency install. `--no-extras` limits the run to what the project's own
packages ship.

## Tests

`bun test ./tests` in the package — all offline. The `./llm` specs are driven by a fake
`LlmFileProvider` and import from the built subpath (`@owlmeans/agent-skills/llm`), so a change
under `src/llm` needs `tsc -b` before they test it. The installer spec imports the CLI sources
directly and needs no build; it drives real files under a temp directory.

## Depends On

- Runtime: nothing (CLI). `./llm`: optional peers `@owlmeans/llm`, `@owlmeans/llm-common`,
  `@owlmeans/agent`, `@langchain/core`

## Related

- [[llm-prompt-caching]] — block order, breakpoints, `claim` and `utility`
- [[llm-common]] — `SkillDefinition`, `PromptBlock`, `LlmFileProvider`
- [[agent]] — the `AgentPlugin` seam and `AgentToolSet` conventions
- [[skill-authoring]] — writing the SKILL.md files this reads
- [[nested-agent-context]] — why embedded `agent-meta/` copies are ignored in a linked checkout

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…