claudeplugins.
claudeplugins7 min read

Claude Code Agent Definitions: When YAML Beats Skills and Commands

A practical guide to Claude Code agent definition YAML — when to reach for an agent vs a skill vs a slash command, plus scoping conventions that keep your .claude directory sane.

Claude Code Agent Definitions: When YAML Beats Skills and Commands

Claude Code ships with three configuration primitives that look interchangeable on first read: agents, skills, and slash commands. They overlap enough that new operators routinely pick the wrong one, then spend a week wondering why their "skill" can't run in the background or why their "command" keeps polluting the parent context window. The differences are real, and agent definition YAML is the primitive most people under-use.

This article walks through what an agent definition actually is, how to write one, and the scoping conventions that decide whether your YAML lives in ~/.claude/agents/ or your_project/.claude/agents/. If you've been writing 800-line slash commands when you should have been writing 40-line agents, this is the rewrite guide.

What an agent definition actually is

An agent definition is a Markdown file with YAML frontmatter that registers a specialized subagent the parent Claude can spawn via the Task tool. The frontmatter declares the agent's name, description, allowed tools, and (optionally) a model override. The Markdown body is the system prompt the subagent runs with.

---
name: schema-validator
description: Validates JSON Schema files against a known set of constraints. Use proactively after any edit to files matching `schemas/*.json`.
tools: Read, Grep, Glob
model: haiku
---

You are a JSON Schema validator. When invoked, read the target schema, check
it against the rules in `docs/schema-rules.md`, and report violations with
file:line references. Be terse \u2014 operators read your output as a punch list.

Three things make this an agent rather than a skill or a command:

  1. It runs in a separate context window from the parent. The parent only sees your final summary.
  2. It has its own tool allowlist independent of the parent's permissions.
  3. The parent invokes it through the Task tool, not through a slash prefix or a skill trigger.

That isolation is the whole point. When the parent agent launches a subagent to chase a research question, the 5,000 lines of grep output the subagent waded through never enter the parent's context \u2014 only the 200-word summary does.

Agent vs skill vs command \u2014 the actual decision tree

The three primitives differ on three axes: who triggers them, how their context is scoped, and what they're optimized to do.

PrimitiveTriggerContextBest for
AgentParent Claude via Task toolIsolated subagent contextOpen-ended research, parallel investigation, scoped tool execution
SkillAuto-triggered by keyword match in user messageInlined into parent contextDomain-specific workflows the user invokes by describing intent
CommandUser types /<name>Inlined into parent contextOperator-driven recurring tasks, deterministic workflows

Pick the agent when you want context isolation. Pick the skill when you want the user to describe intent ("audit my SEO") and have the right playbook auto-loaded. Pick the command when the operator wants a button labeled /deploy.

The clearest tell that you picked wrong: if your slash command's body is \u2265300 lines of instructions and the first thing it does is "read these 8 files and grep for 4 patterns," you wrote an agent dressed as a command. Move it.

The scoping conventions that actually matter

Claude Code resolves agent definitions from two locations, in this order of precedence:

  • Project-scoped: your_project/.claude/agents/*.md \u2014 checked into the repo, shared with collaborators
  • User-scoped: ~/.claude/agents/*.md \u2014 your personal toolkit, available across every project

Project-scope wins on name collision, which matters more than it sounds. If you have a personal code-reviewer agent at ~/.claude/agents/code-reviewer.md and you clone a repo with your_project/.claude/agents/code-reviewer.md, the project version is what runs. This is intentional \u2014 the repo's standards override your personal habits when you're working in the repo.

The convention worth adopting: project-scope anything tied to repo-specific knowledge (a Rust workspace's clippy conventions, a Python uv layout, a particular schema format), and user-scope anything portable (a generic git-history summarizer, a markdown linter, a "draft a commit message" agent).

A subtler rule: if an agent's system prompt references project-specific paths, it has to be project-scoped. The moment your YAML mentions services/api/ or migrations/, it's no longer portable.

The tools allowlist is your blast radius

The tools field in the frontmatter is the most under-used safety control in the spec. By default, an agent inherits the parent's tool permissions. You almost never want that.

A research agent that should never modify files:

---
name: codebase-explorer
description: Read-only codebase research. Returns file paths and excerpts.
tools: Read, Grep, Glob
---

A test-runner agent that needs Bash but not network:

---
name: test-runner
description: Runs the project's test suite and reports failures.
tools: Bash, Read
---

The pattern: list the minimum tools, omit the rest. If the agent later needs Edit, you'll know \u2014 it'll fail loudly rather than silently mutating something it shouldn't have. This is the same instinct that drives least-privilege IAM policies; treat your subagents like service accounts.

Model overrides and when to use them

The model field accepts opus, sonnet, or haiku. Omit it and the agent inherits the parent's model. Override it when the cost or capability profile differs sharply:

  • Haiku for cheap, deterministic work \u2014 file lookups, format validation, schema checks. A 40\u00d7 cost reduction over Opus pays for itself fast on agents you spawn ten times per session.
  • Sonnet for the default research / refactor agent. Same capability tier as Opus on most coding tasks, ~3\u00d7 cheaper.
  • Opus when the agent's job is genuinely hard \u2014 architecture review, multi-file refactor planning, ambiguous requirements analysis.

A monorepo with 50 active agents will spend 80% of its agent-time on agents that should be Haiku. Audit your ~/.claude/agents/ directory once a quarter; most operators discover three or four agents that have been quietly running on Opus for no reason.

Writing the system prompt body

The Markdown body is the agent's standing instructions. Three patterns produce agents that actually work:

Pattern 1: Lead with the role, not the steps. "You are a JSON Schema validator" is a stronger first sentence than "When invoked, do the following\u2026". The role frames every downstream judgment call.

Pattern 2: State the report shape explicitly. Agents that are told "report findings as a punch list, file:line per item, under 200 words" produce reports the parent can actually use. Agents that aren't told the shape produce essays.

Pattern 3: Document failure modes the agent should refuse. If your db-migration-runner agent should never run a migration without a backup, say so in the body, in capital letters, with the WHY. Subagents read their system prompt the way humans read laws \u2014 they look for loopholes when ambiguous.

A 40-line agent body that nails these three patterns outperforms a 300-line one that buries the role in step 4.

When NOT to write an agent

Skip the agent and inline the work in the parent context if any of these hold:

  • The task takes one tool call. Spawning a subagent costs a context-switch round-trip; one-shot tool calls don't earn it back.
  • The parent needs to chain multiple decisions on the result. Subagent results come back as a single message; if you need to iterate, keep it in the parent.
  • The work needs the parent's full conversation history as context. Subagents start fresh \u2014 they don't see what came before.

The rough heuristic: if the work is an investigation that ends in a summary, agent. If the work is one decision feeding the next, parent.

A concrete migration example

Here's what moving a slash command to an agent looks like in practice. The before:

your_project/.claude/commands/audit-tests.md  (380 lines)
  \u2192 reads 12 files
  \u2192 greps for 6 patterns
  \u2192 produces a 400-word report
  \u2192 pollutes parent context with all the intermediate output

The after \u2014 a 60-line agent + a 4-line slash command:

# your_project/.claude/agents/test-auditor.md
---
name: test-auditor
description: Audits test coverage, flags untested public APIs, reports as punch list.
tools: Read, Grep, Glob
model: sonnet
---

You audit test coverage in this repo... [55 lines of role + report shape]
# your_project/.claude/commands/audit-tests.md
Launch the `test-auditor` agent. Pass through any path arguments the user
supplied. Surface the agent's report verbatim.

Parent context stays clean, agent gets a least-privilege toolset, and the slash command becomes the user-facing button without owning the implementation.

Summary signals to watch for

You're using agent definitions well when: your .claude/agents/ directory has more files than your .claude/commands/ directory, most of those agents declare a tools allowlist, parent context windows stay small even on multi-hour sessions, and project-scoped agents dominate over user-scoped ones for repo-specific work.

If you're seeing the opposite \u2014 bloated commands, unrestricted tools, subagent-shaped work running in the parent \u2014 start with the three biggest commands and convert them this week. The pattern catches on fast once you see what a clean parent context feels like.

References: