跪拜 Guibai
← Back to the summary

Three Design Pitfalls in Agent Skill Systems (and How to Avoid Them)

Foreword

What is a Skill? Simply put, it is a skill pack, with a SKILL.md file at its core. When an Agent is working, it loads the corresponding Skill on demand based on the task's needs—for example, if it needs to do UI design or code review, it loads the relevant one.

Sounds pretty simple, right? You might have already opened an AI programming tool, ready to throw in a prompt like "Help me implement a Skill mechanism."

Wait, hold on. Skill sounds simple, but have you really thought through how to implement it in an Agent system? See if you can answer these questions first:

If you haven't figured out these questions yet, then I'll break them down one by one and explain the design of the entire Skill mechanism clearly.

1. Understanding SKILL.md: Header Metadata

Please first check the standard Skill header fields officially provided by Claude Code: https://code.claude.com/docs/en/skills Here, I'll pick a few typical ones to talk about:

Field Function
name Display name
description Shown to the model, determines when to use it
allowed-tools Tool whitelist
disallowed-tools Tool blacklist
model Restricts the model
metadata Custom key-value pairs
agent Specifies the subagent for execution
context Whether to open an independent sub-context during execution

(What a turn is will be explained in a later article; for now, understand it like this: a turn is a session round.)

[Turn 1] You send a message → Model calls a Skill → Skill becomes active
            → Tools in disallowed-tools are removed from the available tool pool
            → In this turn, the model simply "cannot see" Write / Edit, unable to call them even if it wants to

[Turn 2] You send the next message → Restrictions cleared → Write / Edit become available again

Skill Example

A simple scenario, which is also the header metadata for most Skills:

---
name: frontend-ui-engineering
description: Build production-quality user interfaces. Use when building or modifying user-facing interfaces. Use when creating components, implementing layouts, managing state, or when the output needs to look production-quality rather than "AI-generated".
---

A slightly more complex one:

---
name: code-review
description: Use when the user asks to review code, check PRs, or find potential bugs.
allowed-tools:
  - Read
  - Grep
  - Bash(git diff:*)
model: claude-sonnet-4-5
disable-model-invocation: false
license: MIT
version: 1.2.0
metadata:
  scope: project
  agents: [backend-bot, reviewer-bot]
---

(license, disable-model-invocation, version in the example are other fields in the official standard; you can ignore them for a basic version first.)

Conclusion: If you are implementing a basic Skill system, only handling name and description is enough; to expand later, build on top of these fields.

2. Deep Dive into Skill and Agent Interaction

1. How the Agent Invokes a Skill

How does the Agent invoke a Skill? There are currently two mainstream approaches:


SKILL_TOOL = {
    "name": "skill",
    "description": "Load a registered Skill by name and return its complete instructions.",
    "parameters": {
        "skill_name": {
            "type": "string",
            "description": "The name of the Skill to load",
            "required": True,
        },
    },
}

def execute_skill_tool(skill_name: str) -> str:
    skill = find_skill_by_name(skill_name)   # Find by name in the registry
    if not skill:
        return f"Skill not found: {skill_name}"

    body = load_body(skill["path"])          # Main body
    apply_permissions(skill["meta"])         # Apply allowed/disallowed-tools

    # Return a three-piece set, with the body injected into context as a tool result
    return {
        "activation": f"<command-name>{skill_name}</command-name>",  # Activation marker
        "base_dir": skill["base_dir"],        # Root directory, relative paths in the body rely on it
        "body": body,                          # Body instructions
    }              

Note that this return value is not just the content of the Skill.md file, but three things:

  1. activation (Activation Marker): <command-name>{skill_name}</command-name>. This is for the system to see—indicating "this Skill has been called", used for deduplication and state tracking (explained below), and also convenient for the frontend to display call events.
  2. base_dir (Root Directory): The directory where the Skill is located.
  3. body (Main Body): The main content of SKILL.md.
READ_FILE_TOOL = {
    "name": "read_file",
    "description": "Read file content by path.",
    "parameters": {
        "path": {
            "type": "string",
            "description": "File path",
            "required": True,
        },
    },
}

def execute_read_file_tool(path: str) -> str:
    return Path(path).read_text(encoding="utf-8")   

SKILL_TOOL can utilize all metadata fields—fine-grained permissions, execution context, activation markers—suitable for complex Skill systems; ReadFile can only read the full text, simple, enough for the early stages.

A little extra knowledge: A Skill's composition is not just the SKILL.md file. Besides SKILL.md, it can also include knowledge documents, executable scripts, static resources, etc.:

{skill_name}/
├── SKILL.md        # Required: Entry point (YAML frontmatter + Markdown instructions)
├── scripts/        # Optional: Executable scripts (Python / Bash)
├── references/     # Optional: Long documents, specifications, examples
└── assets/         # Optional: Templates, icons, fonts, and other static resources

So the essence of invoking a Skill is actually a tool call: use a read tool to read SKILL.md or other knowledge files, and use the Bash tool to execute scripts.

However, in Codex, I noticed it internally uses PowerShell's cat command to get SKILL.md.

image.png

Claude Code tends to use SKILL_TOOL to complete Skill loading.

2. How the Agent Locates a Skill

The first step is for the system to scan a specified directory, find all SKILL.md files under skill folders, read only the header's name and description, and collect them into a registry.

def register_skills(skill_dir: str) -> list[dict]:
    skills = []
    for path in Path(skill_dir).rglob("SKILL.md"):
        meta = parse_frontmatter(path)  # Only read header name and description
        skills.append({
            "name": meta["name"],
            "description": meta["description"],
            "path": str(path),
        })
    return skills

Then this information is injected into the system prompt. The injected content differs for the two tools:

The display of Skills in the context roughly looks like this:

<available_skills>
  <skill>
    <name>pdf</name>
    <description>Comprehensive PDF manipulation toolkit for extracting text and tables, merging/splitting documents, and handling forms.</description>
    <path>/absolute/path/to/pdf/SKILL.md</path>
  </skill>
 </available_skills>

(<path> is for the ReadFile method to locate the file; if using the Skill tool method, the path stays in the registry and is not exposed to the model.)

Note that only name and description are placed here, not the body. This is the most critical design in the entire Skill system: registration must be lightweight. If the body were stuffed in at this step, the subsequent "matching" and "loading" would be meaningless, and the context would be filled with irrelevant content.

3. Skill Activation Markers: Deduplication and State Tracking

Deduplication: Don't Load the Same Skill Repeatedly

In a single task, the model might repeatedly want to use the same Skill. For example, if a user asks twice in a row "review the code for me again", the model might want to call code-review both times.

Without deduplication, the body of code-review would be injected twice, wasting tokens and adding duplicate content to the context.

With activation markers + deduplication, the system can judge:

code-review has already been activated, don't inject it again this time, just reuse it.

In one sentence: Deduplication = preventing the body of the same Skill from being repeatedly stuffed into the context.

State Tracking: Remember "Which Skills Are Currently Active"

The system needs to maintain a "currently active Skills list" because several things rely on it:

In one sentence: State tracking = the system maintains a table of "who is currently active" to manage permission activation and restoration.


In your Skill system implementation, it roughly looks like:

active_skills = set()          # Set of currently active Skills

def activate(skill_name):
    if skill_name in active_skills:
        return "Already active, skipping"   # Deduplication
    active_skills.add(skill_name)   # State tracking
    apply_permissions(skill)

def deactivate(skill_name):
    active_skills.discard(skill_name)
    restore_permissions()

The active_skills set simultaneously handles both "deduplication" and "state tracking"—it's used to judge duplicates and to know what to restore.

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

用户815933786347

I bow to you, master.