---
title: "Managing skills"
manual: "LLM Foundation"
version: "0.41"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:administration-skills@0.41"
source: "Administration/Skills.rst"
rendered: "2026-10-20T08:48:47+00:00"
---

# Managing skills {#administration-skills}

*Skills* are GitHub-hosted `SKILL.md` files — a YAML front-matter block
with a `name` and `description` plus a markdown body — that nr-llm can
ingest, review, and (from Plan 1b) inject into prompts. You add a **skill
source** that points at GitHub, sync it, and then enable the individual
skills you want.

Skill management is **admin-only**. It lives in
**AI > Authoring > Skills** and is not delegated to other
backend groups: a skill body becomes prompt context, so the two skill
tables are treated as a privilege-escalation surface.

> [!NOTE]
> Ingest — adding sources, syncing and reviewing — is described by
> [ADR-035](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-035@0.41). Attaching enabled skills to tasks and
> configurations and injecting them into text-generation prompts is
> described by [ADR-036](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-036@0.41) and the
> [Attaching skills](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-skills-attach@0.41) section below.

## Source types {#administration-skills-source-types}

A source has one of three types:

-   **`single_file`**

    One `SKILL.md` at a fixed path in a repository. A single, explicit
    admin act — its skill may default to enabled.

-   **`repo`**

    A whole repository. Every `SKILL.md` under the repo root,
    `skills/<name>/`, `.claude/skills/<name>/` or
    `<plugin>/skills/<name>/` is discovered. Discovered skills arrive
    **disabled** for review.

-   **`marketplace`**

    An Anthropic `marketplace.json` index that lists plugins pointing at
    further repositories. Each entry is expanded with the `repo` flow.
    All discovered skills arrive **disabled**.

## Adding a source {#administration-skills-add}

1.  Navigate to **AI > Authoring > Skills**.
1.  Click **New Skill Source**.
1.  Fill in the fields:

    -   ****Title****

        Display name for the source list.

    -   ****Type****

        `single_file`, `repo` or `marketplace` (see above).

    -   ****URL****

        The GitHub URL the type expects (the `SKILL.md` URL, the
        repository URL, or the `marketplace.json` URL).

    -   ****Ref****

        A branch or tag (for example `main` or `v1.2.0`). It is
        resolved **once** to an immutable commit SHA at sync time; all
        bodies are then fetched by that SHA, never by the moving branch.
1.  Click **Save**.

The `pinned_sha`, `sync_status`, `sync_error` and `last_synced`
fields are managed by the sync run and shown read-only.

Sync errors shown by the module and stored on the source mask credential
query parameters and URL userinfo passwords. Fatal-error diagnostics retain
the exception class, source uid and sanitized message and source URL; they
do not retain the raw exception object.

## GitHub token and rate limits {#administration-skills-token}

Unauthenticated GitHub API access is limited to **60 requests per hour**,
which is quickly exhausted by a `repo` or `marketplace` sync. Add a
personal access token (a read-only, public-repo token is enough) to raise
the limit and to read private repositories.

-   The token is set through the **Set token** action on a source,
    **not** typed into a FormEngine field. The source stores a vault reference;
    nr-vault stores the token with envelope encryption. The plaintext token
    is never stored in the source record.
-   When a sync hits the rate limit (HTTP 403 with no remaining quota), the
    source is set to `sync_status = error` carrying the reset time; state
    is not partially corrupted. Add a token and re-sync.

## Host-allowlist prerequisite {#administration-skills-allowlist}

nr-llm enforces an **app-level GitHub allowlist** on every skill request:
the scheme must be `https` and the host must be one of `github.com`,
`raw.githubusercontent.com`, `api.github.com` or
`codeload.github.com`. This is separate from, and in
addition to, the nr-vault SSRF guard.

On hardened instances that restrict outbound HTTP through the global
`HTTP/allowed_hosts` SSRF setting, those four GitHub hosts **must be on
that list**, otherwise every sync fails closed. This is a deliberate
prerequisite — nr-llm never silently bypasses the SSRF guard.

## Syncing and the review flow {#administration-skills-review}

![The Skills module showing a synced marketplace source and the discovered skills with their support badge and enabled state](../Images/SkillsModule.png)

1.  On a source, click **Sync**. The source moves through
    `never_synced` → `syncing` → `ok` / `partial` / `error`.
    The `syncing` state also acts as a lock: a second concurrent sync on
    the same source is refused.
1.  `partial` means the per-sync file-count or wall-time bound was
    reached (large marketplaces); the skills fetched so far are stored.
1.  Discovered skills from `repo` and `marketplace` sources are
    created **disabled by default**. Review each one, then toggle it on
    with **Enable**.
1.  **Re-sync never silently changes an enabled skill.** If a re-sync
    brings a different version of an enabled skill, nr-llm **auto-disables
    it** so you re-confirm before it is used again. A version is the body
    *and* the frontmatter fields the model reads — name, description,
    support status, `allowed-tools` and the `process` marker — so a
    frontmatter-only change upstream, such as a widened `allowed-tools`,
    disables the skill as well ([ADR-214](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-214@0.41)).
1.  A skill that disappeared upstream is marked **orphaned and disabled**,
    never silently dropped, so attachments (Plan 1b) do not vanish.

Deleting a source cascade-deletes its skills.

## The `partial` support badge {#administration-skills-support-status}

Each skill carries a support badge:

-   **`full`**

    The skill is plain front-matter and prose.

-   **`partial`**

    The body or front-matter references scripts, `references/`,
    `assets/` or an `allowed-tools` declaration.

> [!WARNING]
> `partial` is **not** a "safer content" badge. It only signals that
> the referenced scripts and assets are **not executed** by nr-llm
> (which is true for every skill in this release). The prose itself is
> fully untrusted regardless of the badge. Asset references are stripped
> from injected prose purely to avoid dangling instructions, not as a
> security control.

See [ADR-035](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-035@0.41) for the full design and security rationale.

## Attaching skills and injecting them into prompts {#administration-skills-attach}

Enabled, non-orphaned skills can be attached to a **Task** and/or an
**LLM configuration** via the **Skills** field on those records
(only enabled skills are offered). At execution time, for text-generation
operations only — completion, translation and task execution; **never**
embeddings, vision or speech — nr-llm composes unapproved attached skills into a
delimited block and prepends it to the *user* prompt. Approved instruction
versions use the system-message channel described below; the stored
configuration `system_prompt` is never modified.

A configuration has two skill fields:

-   **Skills loaded on demand**. An agent run lists the attached skills
    whose current version is approved as an instruction (and which are not
    process skills) by uid, name and description in the description of the
    builtin tool `load_skill`. When the task matches, the model loads one: its
    section joins the run's system message before the next call, the tool
    result confirms the load without the body, and loading the same version
    again appends nothing. The load re-checks the attachment, the record and
    the approval at that moment, and refuses a section that would leave less
    than 10 % of the model's context budget. A skill attached here that is not
    approved is neither listed nor composed. Tasks, translation and the other
    one-shot paths never load on demand.
-   **Skills loaded always**. Composed into every run, as described
    below. Every attachment that existed before the load modes is one of these.
    Process skills cannot be attached here.

Both fields grant the skills' declared tools (`allowed-tools`) from the
start of a run; loading a skill grants nothing.

> [!NOTE]
> An attachment loaded always is **eager and complete**. The whole skill
> **body** — the entire `SKILL.md` prose after the front-matter, not just
> the `name`/`description` — is written into the prompt *before* the model
> runs. Nothing loads a skill's `references/` / `scripts/` / `assets/`
> (those lines are stripped from `partial` skills, and the files are never
> executed). Such a skill therefore always costs its full body in tokens on
> every run (subject to the budget below).

Composition rules:

-   **Precedence.** Configuration skills are the baseline, task skills are
    additive; the set is the union deduped by source + identifier (the
    configuration wins on a duplicate). The configuration block renders
    first.
-   **Budget.** The untrusted user-message block is bounded by a byte budget;
    when it is exceeded, task-additive skills are dropped before
    configuration-baseline skills and each drop is logged. Approved instruction
    sections are kept in full and count toward the context-window estimate. A
    request that does not fit even then is still sent, with a logged warning:
    the chat path first drops older turns, the completion path sends the prompt
    unchanged. No instruction section is dropped to make it fit.
-   **Integrity.** Each skill's version digest — over the body and the
    frontmatter fields the model reads — is recomputed from the stored fields at
    injection time; a mismatch (tampering or a stale row) drops that skill — it
    is never injected. A row synced before the digest existed is checked
    against its body checksum until the upgrade wizard or a sync writes its
    digest. Name, description and body are therefore read-only in FormEngine.
-   **Process skills.** A skill whose frontmatter sets `process: true` is
    never composed from an attachment; it is skipped with a warning.
-   **Untrusted output.** Skill prose is third-party text; output produced
    under its influence is treated as untrusted and escaped/sanitized where
    it is stored or rendered. Message role is defense-in-depth, not a trust
    boundary.

See [ADR-036](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-036@0.41) for the injection design.

## Writing skills in the backend {#administration-skills-backend}

A source of the type **Written in the backend** holds skills that are
authored in TYPO3 instead of fetched from GitHub ([ADR-214](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-214@0.41)).

1.  Create a skill source and choose the type **Written in the
    backend**. Set its trust level: it decides whether its skills are admitted
    (`skills.minTrustLevel`) and whether an approved version may instruct
    (`skills.instructionTrustLevel`).
1.  In the **Sources** table, click **New skill** on that
    source. Write the name, an identifier unique within the source, the
    description and the body. `Allowed tools` takes a JSON list of tool
    names; leave it empty for no declaration, `[]` allows no tool.
1.  Enable the skill and attach it like any other. Approve a version as
    described below to make it an instruction.

Skills written here are edited through FormEngine and the DataHandler, so
every change is in the record history with a diff and a rollback. Every edit
changes the version: an approved version stops instructing at the next run
until the edited version is approved. Until a version is approved the skill
grants no tools — it counts as a declared empty list — and afterwards it grants
the tools of the approved version it holds, or of its most recent approved
version while it holds an unapproved one; never the ones in the field.
Every unrevoked approval keeps vouching: an author who restores the fields of
an older approved version gets that version back, its tools included. Revoke
an older version when a newer one is meant to replace it.

A skill attached to a configuration or a task keeps restricting the tools of
its runs even when it stops instructing: an orphaned skill — one whose
identifier the sync no longer finds — grants no tools and still restricts, as
does a skill whose source is gone. A skill that is still attached cannot be
deleted; detach it on the configuration or the task first.

Only an administrator enables or disables a skill. A skill the sync disabled
— its version changed, the scanner found an injection, or it was orphaned —
keeps restricting the runs it is attached to until an administrator enables
it again or detaches it; a skill an administrator disabled drops out of those
runs.
Hiding a skill takes it out of every run like an administrator's disable: a
hidden skill neither instructs nor restricts. Like the enable flag, hiding
and unhiding an existing skill is an administrator's, even where a group is
granted the field. A new skill, a copy included, is stored disabled unless
an administrator enables it, and such a skill restricts the runs it gets
attached to — a copied page's configuration, for example — until an
administrator enables it; a new skill an administrator stores with the
administrator's mark — a copy of a skill an administrator disabled — stays
out.

A workspace publish request that would delete a page holding an attached
skill, on it or below it, is refused as a whole: no workspace action in that
request is applied. "Publish all" works in batches, so batches before the
refused one are published. The single publish and "publish all" show the
refusal; publishing or sending a selection to a stage, and the scheduled
auto-publish, write it to the system log only — a staged delete over an
attached skill then holds back every auto-publish of its workspace until the
skill is detached or the delete discarded.

A backend user who is not an administrator writes skills only with the
fields an administrator granted: the source, the allowed tools and the data
class are excluded fields (**Allowed excludefields** of the backend
group). Without the source grant a new skill is stored without a source and
is never composed. The enable flag is an administrator's, even when a group
is granted it: a new skill stays disabled until an administrator enables
it. The **New skill**
button creates the record on the root page, which only administrators may
write; a backend user who is not an administrator creates the skill with
**Create new record** on a page in their web mount.

A skill does not change its kind by changing its source: a skill that a sync
wrote keeps being checked against what the sync wrote, also on a backend
source, so an edit after such a move takes it out instead of becoming a new
version.

The same rule protects every skill's tool declaration: a skill whose source
is missing, hidden or disabled, and a synced skill edited after its sync count
as a declared empty list. They restrict a run's tools instead of widening them.
A process skill counts only in a run that invokes it; attached without an
invocation it adds neither tools nor a restriction.

A backend source cannot be synced.

## Approving a skill version as an instruction {#administration-skills-approval}

By default every skill is untrusted reference data: it is fenced in the user
message and the model is told not to follow it as instructions. An
administrator can approve one **version** of a skill so that it is composed
into the **system message** as an instruction of the installation, behind the
configuration's own prompt and snippets — or appended to the caller's system
message when the caller sends one ([ADR-214](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-214@0.41)).

1.  In the **Skills** list, click the check-mark action of a skill.
    The review page shows the current version, its version digest, the
    source's trust level and the instruction trust level, and — once a version
    was approved — the diff against the most recent approved version.
1.  Click **Approve this version**. The form sends the digest it
    showed; if the skill changed in the meantime, the approval is refused and
    the page shows the new state.
1.  To withdraw an approval, click **Revoke this version** in the
    approval history. A revoked version becomes an instruction again only
    through a new approval, never because a sync brought the same text back.

A version instructs only while **both** hold:

-   an unrevoked approval names exactly this version (digest) of this skill
    from its current source — any change to the body or a covered frontmatter
    field, or moving the skill to another source, needs a new approval; and
-   the source record's trust level is at or above
    `skills.instructionTrustLevel` (extension configuration, default
    `verified`). A value below `skills.minTrustLevel` is read as that
    minimum; a value nobody can read is read as `first_party`. The level is
    read from the source record at every composition, so re-classifying a
    source takes effect at once. A hidden or disabled source vouches for
    nothing.

A run that waits for a human — for a write approval or for typed input —
keeps the approved versions it was composed with. Before it continues, it
checks each of them again: if the approval was revoked, the skill was deleted,
disabled, hidden or orphaned, or the source fell below `skills.instructionTrustLevel` in the
meantime, the run stops with an error that names the skill and the reason, and
no pending call executes. Start a new run to continue without that
instruction.

The fields that decide this — on a skill its source, trust level, checksums,
version digest, enabled and hidden flags, allowed tools, support status,
orphan flag and stored frontmatter; on a source its type, trust level, hidden
and enabled flags, URL, ref, pinned SHA, expected fingerprint and access
token — are **exclude** fields. A non-admin group reaches them only
when an administrator grants them under **Allowed excludefields**.

A version that contains characters a browser renders as nothing but a model
reads — Unicode tag characters, zero-width and direction marks, no-break
spaces and similar — cannot be approved: the approval would bind to text the
approver did not see. The review page lists each such character with its field,
line and code point. Remove them at the source and sync again.

Approving and revoking is restricted to administrators. Every approval,
revocation and refused approval is written to the skill audit trail with the
version digest it concerns. A skill without a version digest yet (synced
before the upgrade, and not yet migrated) cannot be approved: run the upgrade
wizard **Write the version digest onto existing skills** or sync its
source first.

An approval snapshot keeps an omitted tool declaration distinct from a
declared empty list. A malformed or non-list JSON declaration in a stored
snapshot is read as an empty list, so object values cannot become tool names.

> [!WARNING]
> An approved skill is an instruction with the acting user's reach: it can
> steer which tools the model calls, within the run's allow-list and the
> user's TYPO3 permissions. Write tools still need a human approval for every
> call; no skill can switch that off.

## Isolation controls: trust, fingerprint, injection scan, audit {#administration-skills-isolation}

On top of the SHA-pin and checksum controls above, each source and skill
carries the isolation controls introduced in [ADR-061](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-061@0.41).

**Publisher trust level.** Every source is classified — `untrusted` (the
default, for anonymous public GitHub content), `community`, `verified` or
`first_party` (operator-controlled). This is *provenance*, independent of the
`partial` support badge. Each synced skill denormalises its source's level, so
re-classifying a source takes effect on the next sync. The instance-wide floor
`skills.minTrustLevel` (extension configuration, default `untrusted`) gates
use: a skill below the floor is dropped from **both** prompt injection and the
allowed-tools union. Raising the floor to `verified` therefore hides every
community/untrusted skill without deleting it. Trust is *separate from* the
`enabled = false` default — an `untrusted` skill still needs an explicit
enable. An unreadable or mistyped floor falls back to `untrusted`; the value
actually in force is shown under [Effective policy](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-governance@0.41).

**Skill-block byte budget.** `skills.maxBytes` (extension configuration,
default `24000`) caps the composed block that is prepended to the user
prompt. The measure is bytes, not tokens — a deliberate over-estimate, since no
tokenizer is available. When the block exceeds the budget, skills are dropped
from the tail first: task-additive skills go before the configuration baseline,
and every drop is logged as a warning. Lower the value to reserve more of the
model's context window for the conversation itself; raise it if a large
configuration baseline is being trimmed. An empty, non-numeric or zero value
falls back to `24000` — the cap cannot be switched off, so an emptied field
never puts an unbounded block on the wire. The budget is instance-wide and
independent of the model's context window; the per-request window bound is
handled separately by the context-window manager ([ADR-107](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-107@0.41)).

The budget bounds the prompt block only, **not** the `allowed-tools` union
([Gating tools](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-tools-allowed@0.41)). The union is computed
over the effective skills before the block is assembled, so a skill dropped
for the budget still grants its tools while its usage rules stay out of the
prompt. That is deliberate — a budget-aware union would *widen* the gate,
because dropping the last declaring skill removes the restriction entirely.
Watch the drop warnings when lowering the value: they name every skill whose
prose stopped shipping while its tools kept being offered.

**Manifest fingerprint (optional).** A source may declare an
`expected_fingerprint`: the sha256 its whole skill set must hash to. When set,
the digest is recomputed at sync and verified before anything is materialised; a
mismatch fails closed (no skill is imported, the source goes to `error`) and
leaves the last known-good skills untouched. Leave it empty to rely on the
commit-SHA pin alone. This binds the reviewed bytes to a publisher-declared
identity beyond "these bytes from this URL"; it is a declared digest, not a
public-key signature.

**Prompt-injection scan.** Each body is scanned at ingest for known injection
signatures. A **high-confidence** jailbreak marker (e.g. "ignore all previous
instructions", role reset, chat-template control tokens) force-disables the
skill at import — even a single-file source that would otherwise default
enabled — and must be re-reviewed before enabling. Lower-confidence findings are
recorded on the skill (`Injection scan findings`) for review without blocking.

**Immutable audit trail.** Every ingest, enable, disable and fail-closed
rejection is written to `tx_nrllm_skill_audit` with who / when / source / SHA /
checksum / trust level / scan result. Normal ingest and approval operations
never update or delete existing audit rows. The explicit
`nrllm:privacy:purge` command removes rows older than
`privacy.retention.skillAudit`; provenance remains reconstructable within
that configured retention window. See [Data retention & purge](https://docs.typo3.org/permalink/netresearch/nr-llm:administration-data-retention@0.41).
