---
name: nightowls-skill-store
description: Use when importing third-party SKILL.md content into a Night Owls swarm, wiring skill providers such as skills.sh or GitHub, checking imported skills for upstream updates, or mounting the skill-store admin routes.
license: MIT
metadata:
  source: https://nightowls.dev/skills/nightowls-skill-store.md
  verified: "2026-07-26"
---

# Importing skills safely

An imported skill is third-party markdown that lands in your agents' system prompts. Everything
here exists because of that one fact.

Ships in `@nightowlsdev/skills@^0.4.0` with routes in `@nightowlsdev/runner-nextjs@^3.5.0`.

## Wire a registry

```ts
import { createSkillProviderRegistry, skillsShProvider, githubSkillProvider } from "@nightowlsdev/skills";

const registry = createSkillProviderRegistry({
  providers: [skillsShProvider(), githubSkillProvider()],
  policy: { allowAuthors: ["acme"], maxInstructionBytes: 40_000, authorize: assertIsAdmin },
});
```

`policy` is a floor, not a default. A per-call policy can only tighten it: allowlists intersect,
byte caps take the minimum, and both `authorize` hooks run.

`registry.describe()` reports `{ id, displayName, refHint?, capabilities: { canList, canAudit } }`.
`registry.search(query, gate)` returns `{ listings, warnings }`. One provider being down is a
partial result, so render the warnings rather than showing an empty list.

## Check for updates without installing them

```ts
const statuses = await checkSkillUpdates({
  registry, storage, tenantId, actor,
  names: ["seo-writing"],   // omit for every stored skill
  persist: true,            // records the verdict on the head row
});
```

The result is a discriminated union keyed by `name` and `status`: `up-to-date`, `update-available`,
`local-only`, `missing-provider`, `missing-skill`, `name-drift`, `error`. Only the first two carry
`headSourceVersion` and `upstreamSourceVersion`.

This never publishes. Showing "3 skills have updates" must not install them. Use `refreshSkill`
when you actually want to publish.

`persist: true` silently no-ops if your storage adapter has no `recordUpdateCheck`. If badges do not
survive a reload, check that migration `0024` was applied before suspecting the check.

## Mount the routes

```ts
createNextjsRunner({
  engine, auth, storage, guardDefinitionAdmin,
  skillPlane: {
    registry,
    previewSecret: process.env.SKILL_PREVIEW_SECRET!,
    allowlistedHosts: ["raw.githubusercontent.com"],
    previewSkill, importSkill, refreshSkill, refreshAllSkills, checkSkillUpdates,
  },
});
```

The plane throws at construction, not at first request, if `previewSecret` is missing or an
http-family provider has no host allowlist. A gate that quietly stopped gating is worse than no
gate, because operators believe imports are reviewed.

## What will surprise you

`import` will not accept hand-filled parameters. It redeems a short-lived token that `preview`
mints, bound to tenant, actor, name and source version. A flow that skips `preview` is not faster,
it is the bypass this design closes.

An expired token returns 410 with a reason so the UI can offer a re-preview. Every other token
failure returns an opaque 403 on purpose, so an attacker gets no oracle.

An imported skill can never carry tools. The type has no tools field, a code skill always wins a
name clash, and built-in tool names are unclaimable. Instruction text is wrapped in an
`untrusted="true"` fence with delimiters neutralised before it reaches a prompt.

Change of source identity always publishes, even when the text is byte-identical. Otherwise an
authorised source swap would leave head pointing at the abandoned upstream.
