Security Research

License to Skill: You come here, to my place, without references, carrying a piece

License to Skill: You come here, to my place, without references, carrying a piece

License to Skill: You come here, to my place, without references, carrying a piece

Golan Myers

|

|

Reading Time:

5

min

Table of Contents




One File, Thirty-One Interpreters

A SKILL.md file is supposed to be portable. That is the entire premise of the agentskills.io spec - a Markdown body with a YAML frontmatter header, dropped in ~/.agents/skills/ or any agent's native skills directory, recognised by any agent that points at the directory. Author once, run everywhere.

The frontmatter, in particular, is where the skill's contract with the runtime lives. allowed-tools says: these are the tools I should be permitted to invoke. disable-model-invocation: true says: do not let the model auto-trigger me. hooks says: fire this shell command on these lifecycle events. These are not decorative metadata. They are actionable fields, and in some cases, the closest thing the format has to a security policy.

We audited 31 agentic coding clients with SKILL.md skill systems - reading bundled JavaScript, decompiling Go and Rust binaries, dumping Java JAR class structures, and grepping Python source - to see which of those frontmatter fields each client actually honors. Reads, parses, and acts upon.

The answer: almost none of them honor almost any of it.

Looking even further - skill authors do not fully understand the schema. Looking at skills hosted in different public repositories on Github, we observed dozens more fields which are not read by any of the tested agent, such as risk, date_added, tools and compatible_tools.

Universal Schema? Agree to Disagree

The agentskills.io spec defines roughly two dozen frontmatter fields a SKILL.md might declare. Of those, only two are honored across the board: name and description. Every other field is a gamble against the client's parser.

The shape of the gamble matters. A field can fail in five distinct ways, and each failure has a different security consequence:

  • Silent drop. The field is loaded by the YAML parser, never read by the consuming code, never warned about. The skill author has no signal that their declaration did nothing.

  • Lint-only acknowledgement. Recognized as a "valid key" to suppress a warning, but never consulted at runtime. For example, VS Code, Windsurf, and Amp do this for license, compatibility, and metadata.

  • Name remapping. The field is honored, but the values inside it are silently rewritten. Droid (@factory/cli) remaps tool names: BashExecute, WebFetchFetchUrl, WriteCreate. A Claude-authored allowlist referencing standard names is silently ineffective.

  • Conditional honoring. The field is parsed but only registered with the runtime under specific configuration. CodeBuddy honors hooks only when the skill has context: fork set, and even then requires allowUntrustedFrontmatterHooks: true in settings.json.

  • Alias divergence. The same logical field has different spellings across clients. Kimi Code CLI accepts when_to_use, when-to-use, and whenToUse. CodeBuddy uses snake_case and kebab-case interchangeably. Most others accept only one.

None of these are visible to the skill author. There is no --strict flag, no validation step, no "unknown field" warning in most clients. The file loads, the skill activates, and the security properties the author thought they had are simply not there.

Case Study: Allowed Tools

Of the entire frontmatter schema, allowed-tools carries the most security weight. In Claude Code, it is an auto-approval allowlist - listed tools execute without per-call user confirmation while the skill is active. A skill author writing allowed-tools: [Read, Grep] is making an explicit safety statement regarding the tools not in the list: this skill should never invoke Bash or Write; if it tries, the user must approve.

Drop that same skill into 23 other clients and the field disappears. Tabnine destructs only name and description from the parsed frontmatter and returns the rest to the heap. Cline reads only frontmatter.name and frontmatter.description from a typed struct that has no place for anything else. Warp parses the YAML into a generic HashMap<String, String> and then never reads any key but those two.

Worse, the eight clients that do honor the field do not agree on what it means. We isolated four distinct semantics across the survey:

  • Auto-approval allowlist (Claude Code, CodeBuddy, Qwen): Listed tools execute without user confirmation. This is the strongest interpretation - and the one that drops the safety gate. Note that in Qwen the field is allowedTools.

  • Scope restriction (Devin-cli, Amp, OpenHands): The field declares which tools the agent may use; user confirmation behavior is unchanged. A skill claiming allowed-tools: [Read] still blocks Write attempts but does not auto-approve Read.

  • Server-side enforcement (Rovo Dev): Field is parsed client-side but enforced by Atlassian's server; the binary on the developer's machine cannot be relied on to honor it locally.

  • No-op via name remapping (Droid @factory): The field is honored, but the tool names inside it are rewritten at load time. A skill written for Claude Code's Bash tool is silently ineffective because Droid looks for Execute.




Anatomy of a Silent Drop

This is what a silent drop looks like in the wild. The Tabnine CLI's frontmatter parser - extracted directly from the bundled chunk in a local install - destructures exactly two keys and discards the rest:

Tabnine is not an outlier. OpenCode and Kilo Code share an equivalent strict-two-field schema that allowlists name and description and rejects everything else at type-validation time. Cline's SkillMetadata struct has four fields - name, description, path, source - and the loadSkillMetadata() path never touches anything else. Goose's Rust struct allows a single extensible HashMap for metadata but lacks any field for model, allowed-tools, or hooks. The pattern repeats across at least a dozen clients in the survey.

Phantom Constraints: When Safety Boilerplate Becomes Theater

We call this failure mode Phantom Constraints - a frontmatter declaration that looks like a safety policy, parses as valid YAML, ships with the skill across repositories, and constrains nothing at runtime in most clients.

The pattern matters because skill files travel. They are cloned via dotfiles repos, copied between machines, redistributed on GitHub, recommended in blog posts. The author writes disable-model-invocation: true on a skill that is meant to require an explicit slash-command trigger. In Claude Code, that means the model cannot auto-fire the skill. In Goose, Tabnine, Warp, Gemini CLI, Cline, JetBrains Junie, the model can absolutely auto-fire the skill - because the field is invisible to those parsers. The author's intent survived the round trip through GitHub. The enforcement did not.




The disable-model-invocation Lottery

Consider a skill that performs a destructive action - say, rewriting commit history or modifying production config. The author, sensibly, sets disable-model-invocation: true in the frontmatter to ensure only a deliberate user invocation can trigger it. Twelve of the 31 audited clients honor this field at runtime: Claude Code, OpenHands, Qwen, Droid, VS Code/Copilot, Crush, Amp, CodeBuddy, Cursor, Windsurf, OpenClaw, and Kimi Code CLI. In the other 19, the field is silently dropped. The model can autonomously decide to run the destructive skill the moment its description looks relevant to the current task.

And even among agents that honor the same intended field, alias divergence creates a second failure surface. Kimi accepts whenToUse, when-to-use, and when_to_use via an explicit alias table. Most other clients accept only one. A skill author writing in CodeBuddy's camelCase style (disableModelInvocation) and then porting to Crush will find that the kebab-case-only Go YAML tag does not match the alias - the constraint silently disappears, again. The same thing happens in Qwen which only accepts allowedTools as opposed to the other agents such as Claude Code which only support allowed-tools.

Field Drift Across Shared Skill Directories

The fragility compounds when clients share filesystem paths. Several surveyed clients read each other's skill directories - but each uses its own parser. A SKILL.md in ~/.claude/skills/ is loaded not only by Claude Code, but by most clients - with each of them seeing a different subset of the frontmatter:

This is not a theoretical risk. Devin reads from ~/.claude/skills/, ~/.cursor/skills/, ~/.codex/skills/, and ~/.windsurf/skills/. Cursor reads from ~/.claude/skills/ , ~/.codex/skills/ and ~/.agents/skills/. Amp reads from three different paths including ~/.config/agents/skills/. A skill installed for one client surfaces in another without any explicit cross-registration - and the security properties it declared on install do not come with it.

Six Outliers Worth Knowing

Most of the surveyed clients converge on the same minimal pattern - name, description, and a handful of either-or fields. A small number diverge in ways that matter for skill authors and security reviewers.

Claude Code - the Maximalist

Claude Code honors the full universal set plus client-specific extras (such as disallowed-tools, agent, paths, shell, context). It is the only client that honors hooks as a first-class lifecycle declaration. Every safety field a skill author can write is read - and acted on. A SKILL.md authored against Claude Code's parser is also the largest attack surface, because the frontmatter is doing the most work.

Droid (@factory) - the Silent Remapper

Droid honors allowed-tools but rewrites some tool names at load time: BashExecute, WebFetchFetchUrl, WriteCreate (the rest remain unchanged). A skill ported from Claude Code with allowed-tools: [Bash, Write] compiles to nothing in Droid - those names are not in Droid's tool registry. The allowlist is present, honored, and effectively empty.

CodeBuddy (Tencent) - the Conditional Fork

A near-exact fork of Claude Code, CodeBuddy parses every Claude Code frontmatter field. But hooks is registered only when the skill carries context: fork, and only when the user's settings.json has allowUntrustedFrontmatterHooks: true. The frontmatter looks identical to Claude Code's. The runtime behavior diverges sharply based on a flag that is invisible at install time.

OpenHands - Snake_case and Kebab Together

OpenHands accepts both allowed-tools and allowed_tools, both disable-model-invocation and disable_model_invocation, in the same parser. It also adds type and agent fields to route skills to specific agent classes. The flexibility is welcome; the side effect is that skills authored against OpenHands often use snake_case fields that break in every other client.

Kimi Code - the Alias Translator

Kimi maintains an explicit METADATA_ALIASES table mapping kebab-case and snake_case forms onto camelCase internal names. It is the only client surveyed that does this declaratively. Skills authored in any of the three styles work. The alias table is also a useful reference for understanding what Kimi treats as the canonical form.

Codex (OpenAI) - the Field-Set Splitter

Codex is the only client that ships a documented two-file split. SKILL.md frontmatter honors exactly name and description - everything else lives in a sibling agents/openai.yaml file: interface.default_prompt, policy.allow_implicit_invocation, dependencies.tools, and the model selector. A reviewer who checks only the SKILL.md frontmatter sees a minimal, harmless-looking skill; the configuration that actually governs runtime behavior - including the flags that control the skill's MCP server spawning - is in a separate file most static analyzers would not associate with the skill. The frontmatter is honest about what it doesn't carry, but the real attack surface is one directory over.

Strategic Implications

For Skill Authors and Security Teams

Treat frontmatter declarations as documentation, not enforcement. The fields you write describe your intent. They do not, in most clients, bind the runtime.

  • Test the skill in the target client before trusting any safety field. If a skill is meant to run only in Claude Code with hooks armed, do not assume it behaves the same way when Devin or Cursor picks it up from the shared directory. Each client's parser is the authority on what gets honored.

  • Inventory which clients are installed on your developer endpoints. Seven clients honor allowed-tools (and one honors allowedTools); twenty-three others do not. A fleet with mixed clients has mixed enforcement on the same skills.

  • Audit shared skill directories on a per-client basis. A SKILL.md in ~/.claude/skills/ is a different artifact to Claude Code than it is to Augment or Goose or Warp. Review what each reader sees, not what the author wrote.

  • Do not rely on disable-model-invocation for sensitive skills. Nineteen of the audited clients ignore it. The skill that you marked "user-only" may be auto-invoked by any model whose client drops the field.

For Developers Building Agentic Clients

The frontmatter parser is a security boundary. The fields it drops silently are the boundary's failure mode.

  • Warn on unknown fields. A skill author who writes allowed-tools in a client that does not honor it deserves an explicit signal. Treat unknown frontmatter as a compatibility lint, not a no-op.

  • Document the exact field set your parser honors. The agentskills.io spec is permissive; your parser is the contract. Publish the honored field list in your skill documentation, and update it when the parser changes.

  • If you do not honor a field, do not pretend you do. Lint-only whitelists - parsing license just to suppress an "unknown key" warning - are worse than dropping the field outright. They imply enforcement that does not exist.

  • Be cautious with cross-client path inheritance. If your client reads ~/.claude/skills/, your users have skills they did not author for your tool. The frontmatter they trusted in one runtime is not the policy you are providing.

Conclusion

SKILL.md was sold as a portable, declarative format. In practice, the body travels - and the frontmatter does not. A skill author writing allowed-tools, disable-model-invocation, and hooks is documenting intent in YAML. The runtime, in most clients, has no parser for that intent. The Markdown gets read. The constraints get dropped. The skill runs.

This is not, by itself, a vulnerability in any one client. It is a coordination failure across the ecosystem - a shared file format with no shared enforcement contract. Until the parsers converge, the frontmatter you write is a wish, and the runtime that loads your skill is the one that decides whether to grant it.




Appendix I: Support Matrix

The full audit, narrowed to the five security-critical frontmatter fields. name and description are honored by every client and are omitted for brevity. Clients are ordered by how many of the audited fields they honor at runtime - the maximalists at the top, the minimal-parser clients at the bottom.

Appendix II: Per-Client Field Support

The full set of frontmatter fields each parser honors at runtime. name and description are honored by every audited client and are omitted from every row. Clients with no other honored fields are grouped at the bottom.