Skip to content

Agent Skills Are Procedures, Not Project Context

A skill loads when the model matches your task to its description, or when you name it. Keep SKILL.md a procedure and let it read project facts from the repo.

Ayhan Sipahi Ayhan Sipahi

A SKILL.md that hard-codes the deploy command, the cloud region, the staging URL, and the announcement channel is correct in one repository. Claude Code, Cursor, GitHub Copilot, OpenAI Codex, and Gemini CLI now read the same SKILL.md format, and with one symlink for Claude Code, one shared folder. It travels across tools and repos, and every fact baked into it goes stale everywhere else at once. The pull is structural: the skill folder ports and path-scoped rules do not, so facts drift into the one file that moves.

A skill has two triggers: you type its name, or the model matches your task against its description. Neither one fires because a file was touched. Facts that must hold outside one procedure, such as a style guide, need a trigger that always fires or fires on a matching path. Facts that only one procedure needs could load with that procedure. The skill folder is still the wrong home for them. It is the layer that travels between repos and tools, and the same fact usually has other readers. So facts do not belong in skills. A skill is a procedure; it reads the facts from the instruction file, a path-scoped rule, a repo file the instructions point to, or the invocation arguments. A skill’s paths field, covered below, only narrows when the skill is offered.

This post is for people who maintain a skills folder plus instruction files for more than one coding agent. It gives a two-question test for every line in a skill and a home for each kind of context in five tools. It then walks through one release skill before and after, the cases where the rule bends, and a correction to an earlier post on this site.

A Trigger per Layer#

Each layer of agent configuration has its own trigger, and the trigger decides what the layer can safely hold. The always-on instruction file (CLAUDE.md, AGENTS.md, GEMINI.md, or copilot-instructions.md) loads every session. A path-scoped rule loads when the agent works with a file that matches its pattern. Invocation arguments and injected command output exist for one invocation. A skill loads on a description match or a typed name. Claude Code’s comparison table lists its trigger as “You typing /<name>, or Claude matching the description to your task”. Neither trigger is guaranteed in practice: the model can miss the match, and a person can forget the name.

The vendors describe the match as a best effort. Claude Code’s feature overview warns: “If descriptions are vague or overlap, Claude may load the wrong skill or miss one that would help.” VS Code’s Copilot docs: “Discovery makes skills available to the model but does not guarantee that it invokes a skill for every relevant prompt.” Claude Code adds two more ways for skill content to go missing. With many skills, descriptions drop out of the listing to fit a character budget that “scales at 1% of the model’s context window”. After compaction, invoked skills come back “keeping the first 5,000 tokens of each” under a combined 25,000-token budget, while the project-root CLAUDE.md is re-read from disk in full.

Three kinds of context follow from the triggers. World context holds in every repo: how a canary rollout works, how PDF forms are structured, the reference for a public API. Local context holds for this repo, team, or machine: the build command, the deploy target, the locale list, the channel to notify, the style guide. Now context holds for this session: the target environment, the current diff, the version, the ticket. Only the first kind can live inside a skill, and the test for any line is short. Would this line be false in the next repo? Then it is local context. Would it be false in the next session? Then it is now context. Only lines that survive both moves belong in the skill.

Two Official Sentences That Point the Other Way#

Two sentences in official documentation read as permission to store project facts in skills. Both can be answered from the same documentation.

The Agent Skills overview at agentskills.io says: “Skills solve this by packaging procedural knowledge and company-, team-, and user-specific context into portable, version-controlled folders that agents load on demand.” The sentence is right that a skill can carry context; the question is which kind. On-demand loading is a problem only for context that must hold outside the procedure. The standard’s own activation step reads “When a task matches a skill’s description, the agent reads the full SKILL.md instructions into context.” A style guide needed during every edit can be missed that way. A release command is needed only while the release runs, so it loads exactly when it is needed.

For facts that only one procedure needs, two objections remain: portability and duplication. The standard promises that you “Build a skill once and use it across any skills-compatible agent”. A skill holding one project’s deploy command is therefore portable to every agent and correct in one repo. It is also rarely the command’s only reader. CI runs the same command and a person releasing by hand reads the same notes, so the copy in the skill is a second copy. A skill that never leaves its repo removes the first objection and keeps the second. Skill-authoring practice already reads the sentence narrowly. The writing-skills guide in obra/superpowers lists “Pattern applies broadly (not project-specific)” among the reasons to create a skill. Among the reasons to leave one alone it lists “Project-specific conventions (put in your instructions file)”.

The sentence does hold at one scope. A skill distributed across an organization may carry facts that are invariant in every repo it ships to, because the decision test passes at that scope. The next repo is another repo in the same organization, and the line stays true there. The moment one repo needs an exception, that fact moves to the repo’s instruction file, and the skill reads it from there.

The second sentence is Anthropic’s. Claude Code’s feature overview says: “Skills can be reference or action. Reference skills provide knowledge Claude uses throughout your session (like your API style guide). Action skills tell Claude to do something specific (like /deploy that runs your deployment workflow).” A style guide is local context with a path trigger: it applies whenever matching files are edited. A reference skill gives it a semantic trigger instead, so it applies when the model recalls the skill. Other Anthropic guidance points to the path trigger. The memory docs say of a CLAUDE.md entry: “If an entry is a multi-step procedure or only matters for one part of the codebase, move it to a skill or a path-scoped rule instead.” The Steering post goes further: “As the file grows, push team-specific conventions into path-scoped rules and procedures into skills, where they load only when relevant.”

An earlier post on this site, Claude Code Skills and Context Window Bloat, gave the opposite advice in two table rows. For the signal “The task requires your project’s specific conventions” it said “Write a skill that encodes those conventions”. For “Your CLAUDE.md exceeds 200 lines” it said “Audit and move non-universal rules to skills”. Both rows moved facts to a semantic trigger to save always-on tokens, and path-scoped rules save the same tokens without that trade. The corrected rows read: put the conventions in the instruction file or a path-scoped rule and let the skill name them and read them there. When the file grows, move rules that matter for one part of the codebase into path-scoped rules and multi-step procedures into skills, keeping facts out of skills.

Where Each Kind of Context Lives#

All five tools have a home for each trigger type, though the files and the loading rules differ from tool to tool. The table maps the four layers as checked in October 2026, and each cell comes from the vendor page listed in the references. One qualifier applies to the last column. Gemini CLI’s docs carry a banner saying unpaid-tier and Google One users were moved to Antigravity CLI on June 18, 2026. That column applies to the paid tiers that still ship Gemini CLI.

LayerClaude CodeCursorGitHub CopilotCodexGemini CLI
Always-on factsCLAUDE.md, plus .claude/rules/*.md without paths; AGENTS.md when no CLAUDE.md or CLAUDE.local.md exists (v2.1.277 or later) or through an @AGENTS.md import.cursor/rules/*.mdc with alwaysApply: true; root AGENTS.md.github/copilot-instructions.md; AGENTS.md, or a single CLAUDE.md or GEMINI.md at the repo rootthe AGENTS.md chain from ~/.codex/AGENTS.md and the project root down to the working directory; 32 KiB default capGEMINI.md in ~/.gemini/, the workspace, and ancestor directories; context.fileName can add AGENTS.md
Path-scoped facts.claude/rules/*.md with paths:, loaded on Read, Write, or Edit of a matching file; a subdirectory CLAUDE.md once Claude works there.mdc with globs; a nested AGENTS.md for files in that directory.github/instructions/*.instructions.md with applyTo; the nearest AGENTS.md takes precedencea nested AGENTS.md or AGENTS.override.md, chained once at startup from the root to the launch directorya nested GEMINI.md, discovered when a tool accesses that directory
Procedures.claude/skills/, ~/.claude/skills/, nested <subdir>/.claude/skills/, plugins, managed; per-skill symlinks documented, .agents/skills/ not listed.agents/skills/, .cursor/skills/; also reads .claude/skills/ and .codex/skills/.github/skills/, .claude/skills/, .agents/skills/; personal ~/.copilot/skills/, ~/.agents/skills/.agents/skills/ from the working directory up to the repo root; ~/.agents/skills/; /etc/codex/skills.gemini/skills/ or .agents/skills/ (.agents wins within a tier); the same pair under ~
Now context$ARGUMENTS, $ARGUMENTS[N] or $N, named arguments; !`cmd` runs before the model reads the skilltext sent with /skill-name in the same message; no substitution documentedextra context typed after /skill-name; argument-hint; no substitution documenteda $skill-name mention inside the prompt; no substitution documentedthe model calls activate_skill on a description match and the user confirms; no argument channel documented

GitHub Copilot is several surfaces, and its column combines them. GitHub’s support matrix lists, per surface, which instruction files are read, and the lists differ from one surface to the next.

One home is the same in every tool. A fact that only one procedure needs can live in an ordinary repo file. The always-on file points to it in one line, and the skill reads it when it runs. The release example below uses this home.

Claude Code’s auto memory is a recall aid for one machine. Claude Code’s docs say “Auto memory is machine-local.” and describe its files as “not shared across machines or cloud environments”, so team facts stay in checked-in files.

facts

facts

facts

values

Every session

Instruction file

Matching path in scope

Path-scoped rule

Read while the skill runs

Repo file the instructions point to

This invocation

Arguments and injected output

Skill: the procedure only

Description match or typed name

The always-on row is the expensive one, which is what pushes facts outward. CLAUDE.md costs tokens on every request. The memory docs target “under 200 lines per CLAUDE.md file” because “Longer files consume more context and reduce adherence”. Codex stops reading its AGENTS.md chain at project_doc_max_bytes, 32 KiB by default. The path-scoped row absorbs that growth without changing the trigger type, at a different cost. It is the least portable layer: five syntaxes and four semantics. Claude Code and Cursor load a rule on file touch. Copilot matches applyTo globs and prefers the nearest AGENTS.md. Codex uses the launch directory, and Gemini CLI loads on directory access. A glob that never matches fails silently. Claude Code’s InstructionsLoaded hook exists for that failure, “to log which CLAUDE.md and rules files are loaded, when they load, and why.”

The procedures row is the most portable: four tools read .agents/skills/ as it is, and Claude Code reads a per-skill symlink from .claude/skills/. That asymmetry is the reason to keep facts out of the portable layer. The portable copy is the one that arrives in repos where the fact is false. The format ports, but behavior only partly does: the spec’s frontmatter fields carry over, while paths, context: fork, argument substitution, and injection are per-tool extensions. For the instruction-file side of the layout (a shared AGENTS.md, symlinks, and the @AGENTS.md import), see the one-config post.

Name resolution is another reason. Skills override by name, and each tool resolves the clash differently. In Claude Code “Enterprise over personal, and personal over project”, so a teammate’s personal release replaces the project’s in that teammate’s sessions. In Gemini CLI “the version from the higher-precedence location is used”, with the workspace above the user. Codex does not merge them, and both can appear in the skill selector. Always-on files behave differently, since Claude Code’s CLAUDE.md files “are additive: all levels contribute content to Claude’s context simultaneously.” A fact in a skill can be replaced wholesale by a same-named skill; a fact in the instruction file cannot.

The Release Skill Before and After#

The skill from the opening, with every fact baked in:

---
name: release
description: Deploy the web app. Use when the user asks to ship, deploy, or release.
---

# Release

1. Run `pnpm run deploy -- --env staging --region eu-central-1`.
2. Wait for the `deploy.yml` workflow to pass.
3. Confirm `https://staging.example.com/healthz` returns 200.
4. Post the changelog excerpt to `#releases` in Slack.

Each line goes through the two questions:

LineFalse in the next repo?False in the next session?Verdict
pnpm run deploy, eu-central-1yesnolocal context
--env stagingnoyesnow context
deploy.ymlyesnolocal context
https://staging.example.com/healthzyesyeslocal and now context
#releasesyesnolocal context
deploy, wait for CI, check health, announcenonothe skill

Only the last row passes. The after-version keeps the procedure and names the facts it expects; the canonical copy lives in .agents/skills/release/SKILL.md:

---
name: release
description: Release the project to a target environment using the project's documented release process, then verify and announce it. Use when the user asks to ship, deploy, or cut a release.
---

# Release

Target environment: the one named in the request ($ARGUMENTS). If none is named, ask.
Previous release tag (closest tag reachable from HEAD): !`git describe --tags --abbrev=0 2>/dev/null || echo "none"`

The project instructions point to a release doc. This skill expects six facts from it:
the release command, the version source, the CI workflow that gates a release,
each environment's base URL, the health check, and the announcement convention.

1. Read the release doc and find those facts for the target environment.
   If any is missing, stop and ask. Do not guess.
2. Confirm the working tree is clean. Read the version to release from the version
   source and check that it is newer than the previous tag above. If that line still
   shows a command, run it. "none" means this is the first release.
3. If the target is production, show the commands you will run and wait for an explicit yes.
4. Run the release command for the target environment.
5. Wait for the CI workflow to finish. If it fails, report the failing job and stop.
6. Run the health check against the target environment's base URL.
7. Draft the announcement following the project's convention and show it to the
   user before posting.

The phrase the project instructions is tool-neutral on purpose: it means whichever of CLAUDE.md, AGENTS.md, GEMINI.md, or copilot-instructions.md the running tool loads. The skill now holds fact names and fetches the values when it runs. Anthropic’s context-engineering guidance describes the same pattern for agents that keep “lightweight identifiers (file paths, stored queries, web links, etc.)” and retrieve content at runtime. The trade-off shows up in review: a reviewer reads two files instead of one, and the skill cannot be judged alone. In exchange, a missing fact fails loudly, because the skill stops and asks, while a stale embedded fact fails silently. Because a release has side effects, Claude Code’s docs also suggest disable-model-invocation: true (“Use disable-model-invocation: true for skills with side effects.”). Cursor and VS Code document the same field, and Codex uses allow_implicit_invocation: false in agents/openai.yaml. That setting removes the description trigger and leaves only the typed name. It is not a guard, though: a typed /release production still runs. That is why step 3 asks before production, and why the hard gate belongs in CI or a hook, as the next section shows.

The facts move to a release doc in the repo. The always-on file keeps one line that points to it. Here that file is the AGENTS.md that CLAUDE.md imports with @AGENTS.md, or that Claude Code reads directly when no CLAUDE.md exists:

## Release

- Release process, environments, and settings: `docs/RELEASE.md`

Every session pays for that one line, and only a release pays for the values. The release doc holds them per environment:

# Release

- Release command: `pnpm run deploy -- --env <environment>`
- Version source: the `version` field in `package.json`
- CI gate: the `deploy.yml` workflow must pass
- Environments and base URLs:
  - staging: `https://staging.example.com`
  - production: `https://example.com`
- Health check: `GET /healthz` on the environment's base URL, expect 200
- Announcement: post the changelog excerpt to `#releases`

A person releasing by hand reads the same file, so each fact exists once. Two copies of a deploy command drift, and Claude Code’s memory docs warn that “if two instructions contradict each other, Claude may pick one arbitrarily.” Cursor’s rules docs give the general fix: “Reference files instead of copying their contents. This keeps rules short and prevents them from becoming stale as code changes.”

The region leaves the release path entirely. The deploy script reads region and account from infra/config.ts, which is why the release command has no --region flag. If your script takes the flag instead, the release doc says where its value comes from. A path-scoped rule keeps the value in one place when an agent edits infrastructure code. In Claude Code that rule is .claude/rules/infra.md:

---
paths:
  - "infra/**"
---

# Infrastructure

- Region, account IDs, and stack names live in `infra/config.ts`. Read them there; do not copy them into other files.

In Cursor the same rule is .cursor/rules/infra.mdc with globs: infra/** and alwaysApply: false, or a nested infra/AGENTS.md. In Copilot it is .github/instructions/infra.instructions.md with applyTo: "infra/**". In Codex it is infra/AGENTS.md, applied when Codex starts in infra/ or below. In Gemini CLI it is infra/GEMINI.md. The environment arrives as the invocation argument, and the release tag through injection or the command the skill names.

$ARGUMENTS and !`command` are Claude Code extensions; its skills page says “Claude Code extends the standard with additional features like invocation control, subagent execution, and dynamic context injection.” The spec defines no argument substitution, and the other four tools document none. The after-version therefore pairs each placeholder with prose that still works when the placeholder stays literal. The environment is the one named in the request. The tag line is either rendered or, when it still shows the command, run by the agent. In Claude Code the placeholder fills in; elsewhere the prose carries the meaning. Injection has its own trade-off, since it runs shell commands while the skill renders: “Injected commands never prompt for permission while the skill renders. Claude Code checks each one against your permission rules first.” A failing command is worse than a missing value: “A failed command aborts the entire skill invocation, not just its own placeholder.” The || echo "none" fallback on the tag line exists for repos without tags. git describe returns the closest tag reachable from HEAD, not the newest tag in the repository. For a release cut from the current branch, that is usually the base the changelog needs.

Where the Rule Bends#

World context can sit inside the skill folder. A general canary-rollout checklist or the reference for a public library passes both questions. The spec gives it a place: the references/ directory “Contains additional documentation that agents can read when needed”. Two spec notes keep it cheap: “Keep individual reference files focused. Agents load these on demand, so smaller files mean less use of context.” and “Keep file references one level deep from SKILL.md.” The test still separates a public API’s documentation (world context) from your own API style guide (local context).

Skill paths narrow when a procedure is offered. Claude Code describes the field as “Glob patterns that limit when this skill is activated”. Cursor’s version says “When set, the skill is only surfaced when the agent works with files that match.” This offers a procedure in the right part of the codebase; it does not turn the skill body into a fact store. The field is absent from the spec, and Codex, Copilot, and Gemini CLI do not document it.

context: fork is the one case where a skill must fetch its facts at render time. Claude Code runs a forked skill in a subagent, and “The subagent doesn’t see your conversation history, so the skill’s instructions have to stand on their own.” For facts it gets worse: “The built-in Explore and Plan agents skip CLAUDE.md and git status to keep their context small, so a forked skill using agent: Explore sees only the SKILL.md content and the agent’s own system prompt.” The fix is still a pointer: the skill can inject the release line of AGENTS.md at render time with !`grep -A2 '^## Release' AGENTS.md`, or run on general-purpose.

Enforcement belongs in neither file. Claude Code’s feature overview treats an instruction in CLAUDE.md or a skill as a request and says “A PreToolUse hook that blocks the edit is enforcement.” The Steering post agrees that “the enforcement methods are hooks and permissions”. A hook can be scoped to a skill through its hooks frontmatter, and “That hook applies from the time the skill is invoked until the session ends.” The portable form of the same guardrail is a CI check, which applies no matter which tool produced the change.

Auditing an Existing Skill Folder#

The move from the before-version to the after-version generalizes to five steps over a shared skills folder. The first step scans the shared skills for lines that look like local or now context. Typical hits are URLs, chat channels, cloud regions, and tool-specific instruction file names. The second puts each hit through the two questions and moves the line to its home. The third replaces each moved fact with the name of the fact the skill expects, listed once near the top of SKILL.md. The fourth replaces tool-specific file names with the project instructions. The fifth turns the scan into a CI check with a reviewed allowlist, shown below. A machine then enforces the rule, and no sentence in a document has to.

# Lines in shared skills that look like local or now context:
# URLs, chat channels, cloud regions, tool-specific instruction file names
grep -rnE 'https?://|#[a-z][a-z0-9-]+|[a-z]{2}-[a-z]+-[0-9]|CLAUDE\.md|GEMINI\.md|copilot-instructions' \
  --include=SKILL.md .agents/skills

Expect false positives: a public documentation URL or a chat channel named in world context will match too. The two questions sort them during the audit. Once the folder is clean, accepted hits go into an allowlist that gets reviewed like code, and CI fails on any other hit. A missing allowlist fails the check rather than passing it:

# CI: fail on any hit that is not in the reviewed allowlist.
# The allowlist holds one fixed string per line, such as a public docs URL.
hits=$(grep -rnE 'https?://|#[a-z][a-z0-9-]+|[a-z]{2}-[a-z]+-[0-9]|CLAUDE\.md|GEMINI\.md|copilot-instructions' \
  --include=SKILL.md .agents/skills | grep -vFf <(grep . .agents/context-allowlist.txt))
if [ -n "$hits" ]; then printf '%s\n' "$hits"; exit 1; fi

The default holds for every skill and matters most for a folder shared across repos or tools, which is now the normal case. The overrides are narrow. Facts that are invariant everywhere the skill ships can stay, and so can world context in references/. A forked skill fetches its facts at render time, because its agent skips the instruction file. The next step is the audit grep above, run once over the shared folder.

References#

Related posts