Skip to content

Skills

core-agent loads SKILL.md bundles from .agents/skills/<name>/. The schema mirrors Anthropic’s published skills format so existing skill bundles drop in directly.


.agents/
└── skills/
├── echo/
│ └── SKILL.md
├── jira-triage/
│ ├── SKILL.md
│ └── examples/
│ └── ticket.md
└── data-export/
├── SKILL.md
└── helpers/
└── export.py

Each subdirectory of .agents/skills/ is one skill. A skill must contain a SKILL.md at its root; the directory name is the skill’s identifier (and what the agent invokes by). Other files in the directory are referenced from SKILL.md and loaded on demand.


Standard YAML frontmatter + a markdown body. The frontmatter declares the skill’s identity and surface; the body is the prompt content the agent reads when it invokes the skill.

---
name: jira-triage
description: Triage a Jira ticket — read it, classify, recommend next action.
---
When asked to triage a Jira ticket:
1. Read the ticket via the `jira_get_issue` tool.
2. Classify by severity using the rubric in `examples/severity.md`.
3. Suggest the next action: assign, request more info, or close.
Always include the ticket key (e.g. `PROJ-123`) in your response.
FieldNotes
nameSkill identifier. Should match the directory name.
descriptionOne-line summary the agent sees in its tool list. Used by the model to decide when to invoke.
FieldNotes
requiresCapabilities the runtime must actually have for the skill to be loaded at all. See Runtime requirements below.
allowed-toolsPassed through to the ADK skilltoolset. A permission — what the skill may use — where requires is a capability. Deliberately separate.
license, compatibility, metadataPassed through to the ADK skilltoolset.

Anything else the Anthropic SKILL.md spec allows (version, Claude Skills 2.0 extensions) is dropped before parsing, not preserved: the ADK frontmatter parser rejects unknown keys outright, so a bundle carrying them would fail to load at all. The body is untouched either way.


The published image is gcr.io/distroless/static-debian12:nonroot — no shell, no kubectl, no gcloud, no Python. A skill that tells the model to run those loads perfectly happily and then instructs it to do something the runtime cannot do; the model spends turns discovering that, and the operator sees a skill that quietly does nothing.

requires: is how a bundle says what it needs:

---
name: gke-workload-troubleshooting
description: Diagnose a failing GKE workload from events, logs and rollout state.
requires: [shell, kubectl, gcloud]
---

At load time each token is resolved against the live runtime. A skill with an unmet requirement is not loaded — it is withheld from the toolset entirely, so list_skills never mentions it and load_skill cannot reach it — and the daemon says so on stderr at startup:

core-agent: skills: gke-workload-troubleshooting: unmet "requires:" — kubectl (not on PATH); gcloud (not on PATH) — NOT loaded
TokenResolved by
shell (or bash)Whether this build registered the bash tool. Not whether a shell binary exists: the failure this key was written for is a build with --disable-tools=bash on a machine whose /bin/bash is right there.
anything elseexec.LookPath — the binary must be on PATH.

requires: [] means “nothing”, same as omitting the key. A requires: that is neither a string nor a list of strings is a drop too, naming the malformation: a bundle whose requirements cannot be read is unsatisfiable by inspection, and silently loading it would restore the failure the key exists to end.

  • It is not a permission. requires says what must exist; allowed-tools and the permission gate say what may be used. A satisfied requires grants nothing.
  • MCP-server availability is not covered. It is the same shape of claim and a different resolution path; if you need it, say so on #962.
  • There is no warn mode. A withheld skill is already loud and non-fatal — the daemon starts, names the skill and the missing capability, and carries on with the rest of the bundle.

A declarative subagent whose skills: grant names a withheld skill fails with the capability named, not with “unknown skill” — the two have different fixes and must not read the same.

  • Write in second person, addressed to the agent (“When asked X, do Y”).
  • Reference sibling files with relative paths — they’re loaded lazily when the skill is invoked, so a large bundle doesn’t blow up cold-start.
  • Keep frontmatter terse; details belong in the body.
  • Describe how to do the work, not what work to do. See below.

A skill loads at the point of use, so it speaks last — after the system instruction, after AGENTS.md, and after whatever goal a parent delegated. A skill that opens by re-deriving its own task therefore overrides the task the agent was actually given.

That is not hypothetical. A GKE troubleshooting skill opened with:

To begin troubleshooting, acquire the following context from the user or active SETTINGS.md config: Project ID / Cluster Name / Cluster Location / Workload Name … Before running any diagnostics, you must fetch GKE credentials: gcloud container clusters get-credentials …

The subagent that loaded it had been handed a fully specified goal — one named workload, in one named namespace, on one named cluster — and had no operator to ask, no SETTINGS.md, and no shell. So it improvised against its GKE MCP the only way that goes: it enumerated clusters and audited a different one, never touching the workload it was asked about. The subagent’s own persona said “stay scoped to the cluster you were asked about” and lost too.

To stop that, core-agent appends a short framing paragraph to every body served by load_skill. It arrives after the skill’s own text, where recency works for it rather than against it, and it says three things:

  • A skill does not change what you were asked to do or which subject you were asked to do it on.
  • If a step tells you to obtain parameters — from the user, from a settings file, or by discovery — use the ones the task already supplied and obtain only what is genuinely missing.
  • If a step names a tool or command you don’t have, skip it and use the tools you do have. A missing tool is not a reason to change target or widen scope.

This applies to every skill, including those a declarative subagent narrows with subagents[].skills — the subagent case is the one that was reported. Frontmatter and references/ files are untouched: the trailer belongs at the end of the instruction body and nowhere else. The exact wording is skills.InstructionFraming, exported so an embedder writing its own skill.Source can reuse it.

Write skills that don’t need it. A skill body should assume its parameters are already in the conversation, and should name only tooling the recipe’s config actually registers — examples/internal/recipecheck gates the second half of that in CI.


At startup, core-agent reads skills from three roots in precedence order and merges them into one virtual filesystem. On name collision the higher-precedence source wins.

PrecedencePathScope
1 (highest)<agentsDir>/skills/ (typically <project>/.agents/skills/)Project — checked in to the repo
2~/.agents/skills/Portable user assets
3 (lowest)~/.core-agent/skills/User-global fallback

~/.agents/ is the portable user root — the same layout you’d use inside a project’s .agents/ but at $HOME. Drop a skill there once and every project picks it up, even in harness setups (e.g. scion) that pre-create a workspace .agents/skills/ that would otherwise shadow it. ~/.core-agent/skills/ remains supported as a lower-precedence fallback.

The loader:

  1. Stats each of the three skills/ directories. Missing directories are silently skipped (most operators have none or one populated).
  2. Lists frontmatters via ADK’s skill.NewFileSystemSource over the merged view.
  3. Resolves each skill’s requires: against the live runtime and withholds the ones this runtime cannot serve. Requirements are read from the winning copy of a shadowed skill, so a project bundle overriding a user-global one of the same name contributes its own.
  4. If at least one skill survives, builds a skilltoolset over the surviving set and registers it as a single ADK Toolset.
  5. If a permission gate is configured, wraps the toolset with the gate under the skill namespace.

The Skills returned by skills.Load carries:

  • Toolset — pass to agent.WithToolsets(...).
  • Infos []Info — name + description for each loaded skill, suitable for rendering a /skills view in your host.
  • Dropped []Unsatisfied — skill name + reason for each skill withheld at step 3. Print these; a withheld skill nobody mentions is the same silent failure requires: exists to end.

Embedders that build their tool registry after loading skills — as cmd/core-agent does — should pass skills.WithShellTool(tools.BashRegistered(b)) so the shell token is answered against the catalog the build ends up with. Every other caller can leave it unset: the answer is read off the permission gate, which tools.Build has already told.

loaded, err := skills.Load(ctx, agentsDir, gate)
if err != nil { ... }
if !loaded.Empty() {
opts = append(opts, agent.WithToolsets([]adktool.Toolset{loaded.Toolset}))
}

When a gate is supplied to skills.Load, every skill invocation goes through it under the skill namespace. Allowlist patterns look like:

{
"permissions": {
"allow": ["skill:jira-triage", "skill:data-export"]
}
}

The detail string surfaced in prompts is <skill_name> <json-args> (truncated). Skip gating entirely with permissions.mode: yolo.


A .agents/skills/ directory that exists but contains no valid SKILL.md bundles is treated as a no-op (returns an empty Skills). No error, no toolset registered. This means you can scaffold the directory in advance and add bundles incrementally.

The same holds for the fallback roots — an empty ~/.agents/skills/ or ~/.core-agent/skills/ contributes nothing to the merged view, so scaffolding one root before populating it is safe.


  • Hot reload — adding a new skill requires restarting the process.
  • Per-skill permission scopes — gating is skill:<name> granular; sub-tool gating within a skill bundle isn’t exposed.
  • Versioning — SKILL.md may carry a version field, but core-agent doesn’t currently surface or enforce it. Use the bundle’s directory layout (e.g. one directory per major version) if you need version pinning.

These could land in a later milestone if downstream consumers ask. See the Roadmap.