Architecture

Skills: Packaging AI Workflows as Code

Ad-hoc prompting is fine for one-off questions. Repeatable workflows deserve structure. This post dissects how a production harness defines, discovers, loads, and executes skills — reusable AI workflows that turn tribal knowledge into executable automation.

Tin Dang avatar
Tin Dang
Hand-drawn architecture diagram of an AI harness with the Skills layer highlighted

You have solved a problem before. You invested time refining the prompt, adding context, specifying the output format. The AI produced an excellent result. Then the conversation ended, and that carefully-crafted prompt evaporated.

A skill captures that workflow so it never evaporates. It is a markdown file with metadata that tells the harness: when to activate, what to do, what tools to use, and how to execute. Skills turn prompt engineering from a disposable art into a durable engineering artifact.

Anatomy of a Skill

A skill is a directory containing a SKILL.md file with YAML frontmatter:

---
name: code-review
description: "Structured code review against team standards"
allowed-tools: Read, Grep, Glob, Edit
model: sonnet
context: fork
arguments: file_or_pr
---
Review the code at ${ARGUMENTS} against these dimensions:
1. **Correctness** — Does it do what it claims?
2. **Security** — OWASP top 10 violations?
3. **Performance** — N+1 queries, unnecessary allocations?
4. **Accessibility** — WCAG 2.2 AA compliance?
5. **Style** — Matches project conventions?
For each finding, provide:
- Severity (P0-P3)
- File path and line number
- What's wrong and why it matters
- Concrete fix suggestion
Output a structured verdict: APPROVE, REQUEST_CHANGES, or BLOCK.

The frontmatter is not decoration — every field drives harness behavior.

Frontmatter Fields

FieldPurpose
nameDisplay name (defaults to directory name)
descriptionWhat the skill does (shown to the model for invocation decisions)
allowed-toolsRestrict which tools the skill can use
modelOverride model for this skill (inherit = use parent’s)
contextinline (expand in conversation) or fork (sub-agent)
agentAgent type when forked (general-purpose, code-reviewer, etc.)
effortEffort level (trivial, quick, fast, default, ambitious, epic)
argumentsArgument names for ${ARGUMENTS} substitution
pathsGitignore-style patterns for conditional activation
user-invocableWhether users can type /skill-name (default: true)
disable-model-invocationPrevent the model from calling this automatically
hooksHook configuration (onBeforeInvoke, onAfterInvoke)

The Loading Pipeline

Skills come from five sources, loaded in precedence order:

1. Bundled skills (compiled into the binary)
2. Managed skills (enterprise admin, /etc/claude-code/.claude/skills/)
3. User skills (~/.claude/skills/)
4. Project skills (.claude/skills/ in repo, traversing up to root)
5. Additional directory (--add-dir CLI flag)
6. Legacy commands (.claude/commands/ — deprecated)
7. MCP skills (from connected MCP servers)

Deduplication and conflict resolution

When multiple sources define a skill with the same name, the harness deduplicates by realpath(). This handles symlinks gracefully — if your user skill is a symlink to a project skill, it is loaded once, not twice.

If two physically different skills share a name, source precedence wins: managed beats user beats project.

Conditional activation

Skills with a paths field are not loaded immediately. They sit in a conditional pool, waiting for matching files to be touched:

---
name: react-component-review
paths: "src/components/**/*.tsx"
---

When the model reads or edits a file matching src/components/**/*.tsx, the harness activates the skill:

function activateConditionalSkillsForPaths(filePaths, cwd) {
for (const [name, skill] of conditionalSkills) {
const patterns = skill.paths
if (filePaths.some(f => matchesGitignorePattern(f, patterns))) {
dynamicSkills.set(name, skill)
conditionalSkills.delete(name)
}
}
}

This keeps the skill list focused. A React-specific skill does not clutter the model’s context when you are working on Python backend code.

Dynamic discovery

Skills are not only loaded at startup. As the model reads files in subdirectories, the harness discovers new skill directories:

function discoverSkillDirsForPaths(filePaths, cwd) {
for (const filePath of filePaths) {
// Walk up from file's parent to root
let dir = dirname(filePath)
while (dir !== root) {
const skillDir = join(dir, '.claude/skills/')
if (exists(skillDir) && notGitIgnored(skillDir)) {
newDirs.push(skillDir)
}
dir = dirname(dir)
}
}
return newDirs.sort(deepestFirst)
}

This means a monorepo with per-package skills “just works.” When the model starts working in packages/auth/, it discovers packages/auth/.claude/skills/ and gains access to auth-specific workflows.

Execution Modes

Skills execute in two modes, and the choice has significant architectural consequences.

Inline execution (default)

The skill content is expanded directly into the current conversation:

User: /code-review src/auth/login.ts
Skill content injected as system message
Model sees: "Review the code at src/auth/login.ts against these dimensions..."
Model responds within the current conversation context

Inline execution is fast (no sub-agent overhead) and shares the current context (the model sees everything that happened before the skill invocation). The downside: the skill’s prompt and output consume the parent conversation’s context budget.

Forked execution (context: fork)

The skill spawns an isolated sub-agent with its own context:

User: /deep-analysis src/
New LocalAgentTask created
Sub-agent gets: skill prompt + reference files
Sub-agent runs independently (own tool permissions, model, budget)
Result extracted and returned to parent conversation

Forked execution provides isolation: the skill cannot see the parent’s conversation history, and its tool calls do not consume the parent’s context. This is essential for expensive operations like “analyze this entire directory” — you do not want 50K tokens of analysis polluting the main conversation.

The trade-off: forked skills cannot reference what happened earlier in the conversation. They are self-contained by design.

How the harness chooses

async function executeSkill(command, args, context) {
if (command.context === 'fork') {
// Spawn sub-agent
const task = await runAgent({
agentDefinition: command.agent || 'general-purpose',
prompt: command.getPromptForCommand(args, context),
model: command.model,
allowedTools: command.allowedTools,
})
return extractResult(task)
} else {
// Inline expansion
return injectPromptIntoConversation(
command.getPromptForCommand(args, context)
)
}
}

Variable Substitution

Skill content is not static. The harness substitutes variables before execution:

VariableReplaced with
${ARGUMENTS}User-provided arguments
${CLAUDE_SKILL_DIR}Skill’s directory path (for referencing local files)
${CLAUDE_SESSION_ID}Current session ID
`command` (backticks)Inline shell command output

The shell command execution is a powerful feature — a skill can embed dynamic context:

Current git branch: `git branch --show-current`
Recent changes: `git diff --stat HEAD~5`

For security, MCP-sourced skills cannot execute inline shell commands. Only locally-trusted skills (user, project, bundled) can run arbitrary commands.

Bundled Skills

Some skills ship with the harness binary. These are not user-authored — they are part of the product:

registerBundledSkill({
name: 'loop',
description: 'Run a prompt or command on a recurring interval',
aliases: [],
allowedTools: ['ScheduleWakeup'],
async getPromptForCommand(args, context) {
return [{ type: 'text', text: loopInstructions(args) }]
},
files: {
'references/loop-guide.md': loopGuideContent
}
})

Bundled skills use reference file extraction: supporting files are written to a nonce-prefixed ephemeral directory with 0o700 permissions (owner-only). The skill’s prompt includes Base directory for this skill: /path/to/extracted/ so the model can read reference files on demand.

The nonce prefix and strict permissions prevent symlink attacks — a malicious process cannot predict the extraction path or read the files.

Permission Integration

Skills interact with the permission system at two levels:

  1. Skill invocation — Can the model call this skill? Checked against deny rules, then allow rules. If neither matches, auto-allowed if the skill has only safe properties (read-only metadata).

  2. Tool usage within the skill — The allowed-tools field restricts which tools the skill can use. A code review skill might only need Read, Grep, and Glob. By restricting to read-only tools, you guarantee the review skill cannot accidentally modify code.

// Permission check for skill invocation
async checkPermissions(input, context) {
// Check deny rules first
if (matchesDenyRule(input.skill)) return { behavior: 'deny' }
// Check allow rules
if (matchesAllowRule(input.skill)) return { behavior: 'allow' }
// Auto-allow safe skills (read-only tools, no shell commands)
if (isSafeSkill(command)) return { behavior: 'allow' }
// Otherwise, ask user
return { behavior: 'ask' }
}

Skills as Engineering Artifacts

The most important aspect of skills is not their technical implementation — it is their lifecycle:

  • Skills are files. They live in your repository, get reviewed in PRs, and evolve with your project.
  • Skills are shared. Put them in .claude/skills/ and every team member gets them automatically.
  • Skills are composable. A deployment skill can invoke a test skill which invokes a build skill. Forked execution prevents context interference.
  • Skills are testable. Because they are markdown with predictable variable substitution, you can verify their content programmatically.

This transforms AI assistance from a personal productivity hack into a team-level capability. The knowledge of how to review code, debug a specific class of issue, or generate a specific artifact is encoded in skills that any team member can invoke — and get consistent results regardless of how they phrase their request.

Next: Tasks and Concurrency — Background Agents at Work, where we examine how the harness manages multiple simultaneous operations, background agents, and inter-agent communication.

0

Next in this series

Tasks and Concurrency: Background Agents at Work

Continue reading