Skip to main content
Tracking issue: #285 · Milestone: Agent Hub Platform [OSS] · Status: Phase 1 (#888) and the marketplace (#2467) shipped — the local-capability sandbox and the /api/skills UI surface remain proposedShipped: the gaia.skills runtime — SKILL.md parser + validator, the three discovery roots with auditable precedence, progressive disclosure, <skill>/<tool> tool registration, Agent.load_skill / unload_skill, and gaia skill list|info|create|import|export. Plus the declarative skills: / skill_sets: manifest blocks and per-launch skill-set selection (#2466), whose reference implementation is the email agent (which currently ships with its sets commented out — see below); and gaia skill migrate --from openclaw|hermes|auto (#692), which converts a third-party skill to GAIA format at the experimental tier.Also shipped (hub side): the skill catalog lane — POST /publish/skill, the skills/ R2 namespace, server-side validation of the SKILL.md grammar (#2467), and the pre-publish audit gate, which binds a verdict to the skill, version, tier, and bytes it audited (#2468).Also shipped (client side): the marketplace verbs search/install/remove/publish, bundle signing + install-time tier ceilings, skill-lock.json, and the local resolver that turns a declared skills: / skill_sets: block into loaded skills at construction (#2467); plus gaia skill audit, the engine that produces the verdict the hub gate checks (#2468).Still proposed: the local-capability permission sandbox (#1019), plus the /api/skills router and Agent UI Skills panel.This spec defines the architecture and integration model for skills. The on-disk SKILL.md schema (full field reference, permission grammar, tier promotion) lives in Skill Format — this document builds on it rather than repeating it.

Problem

A GAIA agent’s capabilities are compiled in. Tools arrive through hardcoded mixins (RAGToolsMixin, ShellToolsMixin, …) wired into the agent class and shipped inside the wheel. A user who wants a new capability — a domain checklist, a wrapper around an internal API, a specialized procedure — has no path short of forking the agent or editing AMD source. Meanwhile the wider ecosystem has converged on a portable format for exactly this: the Agent Skills open standard, used by Claude Code and a growing set of agent runtimes. A skill is a folder with a SKILL.md that the agent loads only when relevant, so a library of capabilities costs almost nothing until used.

Goal

GAIA agents compose skills at runtime the same way they compose tools today — declaratively, per-agent, least-privilege — and the format is the Agent Skills open standard so the existing ecosystem of skills works in GAIA unchanged. A plain Claude Code skill drops into ~/.gaia/skills/ and runs; a GAIA skill adds typed tools, a permission model, and security tiers on top of that baseline.

Skill vs Tool vs Agent

These three are layered, not interchangeable. The distinction is the foundation of everything below. The one-line model: a tool is a function, a skill is a packaged capability an agent composes, an agent is the loop that decides when to use it.
A skill is not an agent: it has no loop, no model, no autonomy. It is the reusable middle layer the agent system is currently missing.

Anatomy of a skill

A GAIA skill is a directory whose only required file is SKILL.md. Everything else is optional and loaded on demand.
This shape yields two flavors that sit on one spectrum:
Frontmatter + Markdown body, no code. The body is context, not a function — a procedure, checklist, or domain knowledge injected into the agent when the skill triggers. This is the plain Agent Skills shape and is byte-for-byte compatible with Claude Code skills.
A single skill may be both — instructions and tools. The runtime treats the two flavors uniformly; the only difference is whether a tools block (and its backing code) is present.

Manifest format

The baseline is the Agent Skills standard: name and description are the only required frontmatter fields, and description is the trigger signal the model reads to decide relevance. GAIA layers a superset on top — version, permissions, typed tools, security tier, requirements — defined in full in Skill Format. The mapping: GAIA-specific fields are nested under metadata.gaia so a standard runtime ignores them losslessly — the full grammar is in Skill Format. The standard’s optional compatibility and allowed-tools keys are not part of GAIA’s adopted base (they overlap metadata.gaia); a skill using them still parses, and GAIA ignores them. The compatibility rule: GAIA reads a bare standard SKILL.md as a valid instruction-only skill. Missing GAIA fields take conservative defaults — security_tier: experimental (the most restrictive tier — installing one takes an explicit --allow-experimental), no tools, no permissions. A skill author adopts GAIA features incrementally; nothing is required to make an existing standard skill load.
Division of authority: this spec owns the integration surface (discovery, scoping, progressive disclosure); Skill Format owns the field grammar (the SKILL.md schema, permission grammar, tiers). Both adopt Agent Skills (agentskills.io, as implemented by Claude Code) as the base standard, and treat Hermes and OpenClaw as compatible third-party formats nested under metadata.<vendor>.

Discovery, loading, and scoping

Discovery locations

v1 ships three roots, highest precedence first. A later root never overrides a skill of the same name found earlier. Roots 1–2 are native GAIA. Root 3 lets an existing Claude Code skill library work in GAIA with zero migration (see Compatibility). Precedence is auditable, not silent: a shadowed lower-precedence copy stays visible via SkillManager.shadowed() and is reported by gaia skill list and gaia skill info. A skill directory that fails to parse is likewise surfaced (SkillManager.discovery_errors, a non-zero exit from gaia skill list) rather than quietly omitted.
Deferred to a later phase — not dropped:
  • Project-local ./.gaia/skills/. A per-working-directory native root. It would slot in above the user root, so adding it later only ever narrows scope; nothing authored against v1 changes meaning. Do not add it to the runtime without updating this table.
The user root is also the registry-lock root: gaia skill install writes ~/.gaia/skills/skill-lock.json beside the skills it installs (#2467). The lock is provenance, not a fourth discovery root — discovery still reads the directory, and a skill created or imported locally simply has no lock entry.

Progressive disclosure

A skill is loaded in three levels, mirroring the standard — long reference material costs nothing until it’s actually needed:
1

Metadata (always in context)

name + description of every discovered, in-scope skill are listed for the model. This is the only always-resident cost — a few tokens per skill.
2

Body (on trigger)

When the model judges a skill relevant (its description matches the task) or the user invokes it explicitly, the SKILL.md Markdown body is injected and the skill’s declared tools are registered into the agent’s tool registry.
3

Resources (on demand)

Files the body references (scripts, reference docs, templates) load only when the agent reads or executes them — not at trigger time.

Scoping into an agent

A skill is never globally active. It is scoped to an agent two ways:
  • Declared — either imperatively, by calling load_skill (typically in _register_tools), or declaratively, via the skills: / skill_sets: blocks in gaia-agent.yaml, which every Agent subclass resolves at construction — so a custom harness consumes an installed hub skill with no code change. Either way the agent points SKILL_DIRS at any skills/ folder it bundles.
  • Granted — a skill carrying connector-bridged permissions contributes ConnectorRequirements to the agent, reusing the per-agent grant model from the connectors framework. An agent only ever sees skills explicitly in scope (least privilege); discovery does not imply activation.
At runtime an agent resolves its skill set, then loads each per the disclosure levels above:
Agent.load_skill(name, *, manager=None) -> Skill does exactly this, in this order:
  1. Resolves name across the roots by precedence (SKILL_DIRS~/.gaia/skills → read-only .claude/skills) using the agent’s lazily-built skill_manager. Pass manager= to resolve against a different SkillManager.
  2. Parses and validates the manifest, body included.
  3. Gates permissions before anything is registered — a refused skill leaves no tools and no prompt fragment behind (see Permission & security model).
  4. Registers the skill’s @tool functions from tools.py under <skill-name>/<tool>, so two skills may both provide search_web.
  5. Merges any resolved ConnectorRequirements into REQUIRED_CONNECTORSper instance, never mutating the ClassVar, so one agent’s skill never leaks requirements into a sibling.
  6. Rebuilds the system prompt, injecting the body via get_skills_system_prompt().
Loading is idempotent (a second load_skill of the same name returns the already-loaded Skill) and reversibleunload_skill(name) removes the tools and the prompt fragment, returning True if the skill was loaded. loaded_skills is the {name: Skill} view of what is currently active. A manifest whose declared tools don’t match its tools.py fails loudly and loads nothing: not a missing tool, an undeclared extra tool, a parameter-name mismatch, a required/optional flip, or a type contradiction. There is no partial load — the tool registry is restored byte-for-byte on any failure.
Tier ceilings are enforced at install, not at load. gaia skill install verifies the bundle’s signature, collapses the claimed tier to the one that signature earned, and refuses a permission above the resulting ceiling (#2467). What is still absent is the load-time ceiling and the run-time sandbox behind it (#1019): a skill placed in a discovery root by hand — gaia skill create, gaia skill import, or a plain cp — never passed through install, so load_skill gates on domain (below), not on tier.

Invocation

Once a skill is in scope, it is triggered either by the model (description match — automatic) or explicitly by the user. load_skill in code is the shipped explicit path; a user-facing /web-research slash command follows the Agent UI panel and is not yet available. This matches Claude Code’s dual-invocation model. The standard’s allowed-tools/disallowed-tools keys are parsed but not used as a permission mechanism (see Skill Format) — GAIA’s permissions come from metadata.gaia.

Skill sets

One agent, more than one job. The same email assistant should behave differently against a personal mailbox than against a work one; the same code agent should carry different conventions in different repositories. Hardcoding load_skill calls forces the author to pick one behaviour, or to ship two agents. A skill set is a named, mutually-exclusive bundle of skills, exactly one of which is active per launch. It is declared in gaia-agent.yaml, layered on top of an always-on skills: list:
The field grammar (reference shapes, version, required, set-name rules, and every fail-loud parse error) is specified in Skill Format. This section owns selection. Sets overlap; they are not a partition. inbox-triage above belongs to both sets — a shared capability stays available in every context it applies to. Only the always-on list is exclusive: a set may not re-declare a skill that is already always on.

Selection order

1

Explicit — the generic override

The skill_set= argument to Agent.__init__, surfaced by an agent’s entry point as --skill-set <name>. Highest precedence, and never second-guessed — an explicit request is not re-examined against agent state.
2

The agent's selector hook

Agent.select_skill_set() -> str | None. Override it to key the active set off something the agent already knows — a connected account’s type, a workspace mode, a device profile. Returning None means “no opinion” and defers to the default. Consulted only when nothing explicit was passed.
3

`default_skill_set`

The manifest’s declared default. Required whenever skill_sets is non-empty, so a launch that selects nothing still resolves a set explicitly rather than running with an arbitrary one.
resolve_skill_set() returns which set won and which rule chose it (explicit / selector / default / none), so “why is this set active?” is answerable from a log line rather than by reading code.
An undeclared set name never falls back. Whether it came from --skill-set or from a selector hook, a name the agent does not declare raises SkillSetError naming the valid sets. Launching with the wrong capability bundle is worse than not launching — a work mailbox quietly handed the personal set is precisely the silent-wrong-answer this indirection exists to prevent (GAIA’s no-silent-fallbacks rule).The same applies to a missing signal. If a selector cannot determine the context, it must return None — which resolves the declared default explicitly — rather than guess a set.

Loading

load_skill_set() runs at the end of Agent.__init__ and loads the always-on list plus the resolved set, in declaration order, each through the same load_skill path (and therefore the same permission gate). A skill declared required: false that no discovery root provides is logged and skipped; every other failure propagates. Calling it again with a different name switches sets: skills the previous resolution loaded and the new one does not declare are unloaded first, so a stale set never bleeds into the prompt. active_skill_set is the current answer, or None for an agent that declares no sets — which is every agent that has not adopted the blocks, whose behaviour is unchanged.
A skill body costs context. Loading a set injects each skill’s Markdown body into the system prompt, which is prompt budget the agent’s own tool results no longer have. An agent with a tight context envelope must account for the loaded set’s cost, not assume it is free — the email agent’s bulk-triage path subtracts it from its result-envelope budget for exactly this reason — and its three-skill personal set still cut that envelope from 6,144 tokens to 4,810 (the four-skill work set, to 4,070). Budget the set, and keep bodies short.

Reference implementation: the email agent

The email agent (hub/agents/email/) is the worked example in the tree. It bundles six instruction-only skills and its manifest carries two sets — personal (triage, newsletter digests, travel itineraries) and work (triage, meeting scheduling, action-item extraction, escalation routing) — with inbox-triage deliberately in both.
It ships with those blocks commented out, pending an eval that shows the skills improve triage, so the released agent resolves no set and loads no skills — active_skill_set is None, loaded_skills == {}, and a --skill-set request is a startup error. Treat it as a wiring reference, not as evidence that a shipped agent has a set active.
Its selector maps the connected mailbox’s account type onto a set: a personal Microsoft account gets personal, a work/school account gets work. The kind is derived at connect time from the id_token tid claim (personal accounts always carry the well-known consumers tenant; work/school accounts carry the organization’s Entra tenant id) and recorded on the connection, so no agent re-derives it. A Gmail mailbox has no equivalent claim, so its kind is genuinely unknown and the selector returns None — the manifest’s default_skill_set applies, explicitly. For a step-by-step author walkthrough see Composing skills and skill sets.

Permission & security model

Instruction-only skills carry no code, but they are not free of risk — a body is injected into the model’s context and can attempt prompt injection. Tool skills carry the full risk of the code they run. The model gates both.
The shipped rule: connector-bridged permissions only. Everything else is refused, not faked.A skill declaring any local-capability permission raises SkillPermissionError at load time, naming the offending grants and pointing at #1019. It is never silently loaded and never wrapped in a sandbox that does not exist — an unenforceable grant is a refusal, not a warning. To load such a skill today, drop those permissions and use the agent’s own tools for local access.<domain>:none is an explicit denial — it asks for nothing, so it is always inert: filesystem:none loads fine and produces no requirement. Refusing it would reject a skill for declaring less than the default.
How the bridge resolves. There is no second grant ledger — a bridged permission becomes the same ConnectorRequirement an agent already declares via REQUIRED_CONNECTORS:
  • mcp:connect:<connector-id> → a requirement for that connector. The scope is mandatory and must name a connector in the live catalog: a bare mcp:connect, or one naming an unknown id, fails loudly with the available ids and a pointer to gaia connectors list.
  • network:<level>:<scope> where the scope names a catalog connector → a requirement for that connector, scoped to the level.
  • network:<level>[:<scope>] with no matching catalog connector → a requirement against the reserved network pseudo-connector-id. This is a declaration surface only: it records the egress the skill asks for so gaia skill info and a future egress policy can read it. Phase 1 does not enforce network egress.
Security tiers gate what a skill may do. Every skill resolves to one of verified / community / experimental (defaulting to experimental). The tier sets the permission ceiling and the grant behavior — auto-grant, prompt, or an explicit --allow-experimental opt-in — and gaia skill install enforces it, as defined in Skill Format → Permission model and Security tiers. A tier is decided by the bundle’s signature, not its front-matter claim: the claimed tier is collapsed to the one the signature earned.
A tier is provenance, not containment. experimental is not “sandboxed” — there is no sandbox in v1. A skill’s tools.py runs in the agent’s own process at every tier, which is why local-capability permissions are refused outright rather than granted to a trusted-looking skill.
Highlights as they apply at the agent boundary: This ties the skill layer to the broader security model: skills are a grant surface, audited and scoped per agent, not a global capability.

Agent Hub integration

Skills are first-class Agent Hub artifacts, distributed and versioned alongside agent packages from the Agent Hub Restructure. They are a fourth catalog lane on the existing hub — not a second registry: the same Worker, the same R2 bucket, the same index.json, the same signing and immutability rules (#2467). Distribution. A skill ships one of two ways:
  • Standalone — from the hub with gaia skill install <name[@range]>, which resolves a SemVer range against the catalog, verifies the artifact checksum and the bundle signature, enforces the tier ceiling, and records the outcome in ~/.gaia/skills/skill-lock.json (#2467, #647); publish the other direction with gaia skill publish. Off-hub, gaia skill import <folder|zip|url> copies a skill in (always stamped experimental — no hub provenance to trust) and gaia skill export <name> produces the bundle. See Skill Format → CLI.
  • Bundled inside an agent package’s skills/ directory — version-pinned to that agent, no separate install. Shipped: the agent points SKILL_DIRS at it and the folder becomes the highest-precedence root.

The catalog lane

A published skill is a catalog entry with type: "skill", served from the same GET /index.json as every other package. Segmenting a Skills lane is a filter on type, not a second document — which is why adding the lane broke no existing consumer. A skill entry reuses the shared catalog keys where the meaning is identical — name, description, latest_version, security_tier, permissions — and nests everything skill-specific under skill_metadata: the tools it provides, the tools_required it consumes, its requirements block, and the audit record below. Its readme carries the SKILL.md body, since for a skill that markdown is the package’s primary doc.
A consumer that lists installable agents must filter on type. A skill installs into ~/.gaia/skills/, not through the agent installer, so rendering every catalog entry as an agent offers an install path that cannot work. Package ids are one namespace across all lanes: a skill name can never collide with an agent id (the hub rejects the publish).
Publish gates. Two rules apply to the skill lane specifically:
  • A SemVer version is required to publish, and 0.0.0 — the format’s “unversioned” sentinel — is rejected, so it can never become an immutable release.
  • Skills enter the catalog by pull request. Publishing is not open to the public — the Worker authenticates against a maintainer-held PUBLISH_TOKENS secret, the same curation model agents use. A contributor opens a PR against skills/community/<name>/, the audit runs as a required check, and a maintainer publishes after merge. Merging is not itself a tier promotion.
  • The pre-publish security audit (#2468) gates the tier a skill may claim. community and verified are refused without a cleared report, and a BLOCK verdict is rejected. A REVIEW verdict is currently refused too: holding it needs a quarantine lane and a maintainer sign-off path, which do not exist yet (#2675) — refusing beats admitting an unreviewed skill. An experimental skill may publish without a report and is recorded as unaudited rather than stamped as having passed, so the catalog never implies an audit that did not happen. The verdict is bound to what it audited — skill, version, claimed tier, and a digest of the SKILL.md — so a report cannot be replayed onto a different publish, and a new version re-earns its verdict. The claimed tier must appear in the report’s cleared_tiers, which is what makes “cannot claim a tier it did not clear” enforced rather than recorded. See Skill Format → Pre-publish security audit.
The gate resists replay, not forgery. The audit report is attached by the publisher and is not yet signed, so nothing proves it was produced by a trusted engine. Binding it to the skill, version, and audited bytes closes replay (#2468); making it unforgeable needs a signing key the publisher cannot mint (#1710). Until then a recorded verdict is marked publisher-asserted — it means self-consistent, not vouched for.
Verification. Install-time integrity uses the same mechanism as agents: the per-skill manifest.json records the Worker-computed SHA-256 of every published artifact, and the installer checks the bytes it downloaded against it. Versioning. Skills are SemVer’d independently of the framework and of the agents that use them. Major = breaking tool-signature or permission change; minor = additive; patch = fixes. Agent manifests pin ranges. Each version re-earns its audit verdict. The gaia-agent.yaml link. An agent declares the skills it composes in its manifest, and the base Agent resolves and loads them at construction via load_declared_skills() — see Skill sets:
Set SKILL_MANIFEST to the manifest’s path to opt in, or leave it unset and the agent auto-detects a gaia-agent.yaml beside its module. A skills/ folder in either of those two places is auto-detected the same way, so the skills a package ships resolve for every class in it — SKILL_DIRS is only needed for a bundled folder somewhere else. An agent that sets neither is unaffected. The block resolves the way dependencies: resolves agent-to-agent links: topological order (a skill providing a tool another declares in tools_required loads first), the highest installed version satisfying the range, and fail-loud on a version conflict or a circular dependency. Resolution is local — the skill must already be installed, so an agent never pulls code from the network at startup. A missing required: true skill raises with the gaia skill install command that fixes it; a missing optional one is logged and skipped. skill_sets: — naming a reusable group instead of individual skills (#2466) — expands to the same requirements and resolves through the same path, so there is one resolver.

Agent Skills / Claude Code compatibility

GAIA implements the Agent Skills open standard as its baseline format, so reuse is bidirectional and lossless for the common case. Standard skill → GAIA. A folder with a name/description SKILL.md loads as an instruction-only skill with no changes. Claude Code skill libraries in .claude/skills/ are discovered directly (read-only import root). gaia skill import <path> copies one into ~/.gaia/skills/ and stamps it experimental for explicit promotion. Third-party format → GAIA. Hermes and OpenClaw/ClawHub nest their fields under metadata.<vendor>, the same pattern metadata.gaia uses, so gaia skill migrate --from openclaw|hermes|auto reads the foreign namespace and writes a metadata.gaia block (#692). Fields GAIA models are consumed; the rest stay under metadata.<vendor> and are reported. Migrated skills land experimental, and one needing a local capability GAIA cannot yet enforce is refused with the reason rather than downgraded — see Skill Format → Cross-format compatibility. GAIA skill → standard runtime. A GAIA skill degrades gracefully: the frontmatter name/description and Markdown body are standard, so the instructions work anywhere. Bundled scripts run if the host can execute them. GAIA-specific frontmatter (permissions, security_tier, typed tools) is ignored by runtimes that don’t understand it rather than breaking the parse. What each side adds: The principle: be a strict superset of the open standard. Anything that runs as an Agent Skills skill runs in GAIA; GAIA adds the enforcement and typing the standard intentionally leaves open.

Non-goals

  • Not a new format. GAIA does not invent a skill format; it adopts Agent Skills and extends the frontmatter. A competing schema is explicitly out of scope.
  • Not a replacement for tools or mixins. Inline @tool functions and framework mixins remain valid for agent-specific, non-distributable capability. Skills are for the reusable, portable middle layer.
  • Not autonomous. Skills add capability to an agent’s loop; they do not run their own loop or make their own model calls.

Open questions

  • Tool-skill execution in foreign runtimes. How much of a GAIA tool skill should degrade vs. fail when run by a host that can’t enforce its permissions?
  • Body-injection trust. Resolved by #2468. Every skill’s body is scanned for injection patterns at publish, and the claimed tier decides the consequence rather than a separate policy: advisory at experimental, forcing REVIEW at community, and gated on a medium at verified. Run it yourself with gaia skill audit ./<skill>/. The precision trade-offs (fenced text is downgraded one step rather than ignored; a prohibition is not a directive) are recorded in Skill Format → Open questions.
  • Versioning of bundled vs standalone. Reconciling an agent-pinned bundled skill with a newer standalone install of the same name.