Agent skills — turn policy into a tested workflow
PLAn agent skill is a versioned folder with a SKILL.md file (name, description, instructions) and optional scripts and references, which Claude Code, Codex and Cursor load only when a task matches the description. A skill encodes policy only when it has an owner, acceptance runs that prove it fires, and a hard control behind every rule that must never break.
Your security checklist lives on a wiki page the agent has never read. Last sprint an agent shipped an invoice endpoint with no ownership check, and the reviewer missed it in a 900-line diff. Someone has since installed a dozen marketplace skills, and nobody can say which of them loaded or what they are allowed to run. This page is for the developer who writes the policy skill and the tech lead who owns it once it is in the repository.
Scorecard Q6: How do you encode and enforce organizational policies (brand, security, compliance, UX)?
Maximum-score answer: custom, versioned skills in the repository, owned by the policy’s owner and updated centrally. This page adds the proof: acceptance runs that show the skill fires, stays quiet, and does not overstep.
What an owned policy skill gives you
Section titled “What an owned policy skill gives you”- One
SKILL.mdin.agents/skills/that Claude Code, Codex and Cursor all discover, with no copies to drift apart. - A description that says when the skill applies and when it does not, so it loads on the right tasks only.
- Three acceptance runs (positive, negative, boundary) scripted from the terminal and graded without reading the transcript.
- A
CODEOWNERSentry with required review, so only the policy owner merges a change, and a deny rule that stops the agent’s own edit tools from rewriting it. - A split between advice (the skill) and enforcement (permissions, hooks, CI) that holds when the model ignores the advice.
Does this policy belong in a skill?
Section titled “Does this policy belong in a skill?”A skill is progressive disclosure: every session sees only the name and description, and the body loads when a task matches. That makes it right for procedures that are long, recurring, and relevant to some tasks only. It makes it wrong for anything that must hold on every task.
| The policy is… | Put it in | Why |
|---|---|---|
| A short fact every task needs (“we use pnpm”, “money is integer cents”) | AGENTS.md or CLAUDE.md | It must be in context every time, not on a match |
| A multi-step procedure for some tasks (authorization review, migration checklist, brand copy review) | A skill | Loaded only when relevant, so it costs no context elsewhere |
A rule that must never break (no production credentials, no edits to migrations/) | A permission rule or hook, then CI | A model can skip an instruction; it cannot skip a denied tool call |
| Live data from another system (tickets, dashboards, schemas) | An MCP server | Skills carry instructions and files, not a live connection |
Most real policies split across rows. The authorization example below uses a skill for the review procedure and a CI check for the part that must always hold.
Where do Claude Code, Codex and Cursor look for project skills?
Section titled “Where do Claude Code, Codex and Cursor look for project skills?”The format is an open standard, published by Anthropic on 2025-12-18; the specification’s showcase listed 46 client products on 2026-09-26 (agentskills.io). The discovery paths still differ per tool.
| Tool | Project path it reads | How it was checked |
|---|---|---|
| Claude Code 2.1.283 | .claude/skills/<name>/SKILL.md (symlinked folders are followed) | Claude Code skills docs and a local run, 2026-09-26 |
| Codex CLI 0.157.1 | .agents/skills/<name>/SKILL.md in the repository, plus ~/.agents/skills/ | codex-rs/ext/skills/src/host_roots.rs at tag rust-v0.157.1, 2026-09-26 |
| Cursor | Supports Agent Skills; the skills CLI 1.7.0 installs Cursor project skills into .agents/skills/ | Cursor skills docs (fetched 2026-08-28) and the vercel-labs/skills README |
Claude Code 2.1.283 does not read .agents/skills/ (its documented locations are .claude/skills/ at enterprise, personal, project, nested and plugin level; checked 2026-09-26). So keep one canonical copy in .agents/skills/ and point Claude Code at it with a symlink. Run this at the repository root:
mkdir -p .agents/skills/authz-review/references .claude/skillsln -s ../../.agents/skills/authz-review .claude/skills/authz-reviewgit add .agents/skills/authz-review .claude/skills/authz-reviewThe skills CLI does the same thing when it installs a third-party skill: it keeps one canonical copy and symlinks each agent’s directory to it, or copies with --copy where symlinks are unsupported. To install one skill for all three tools, run npx skills add <owner>/<repo> -a claude-code -a codex -a cursor (skills 1.7.0, checked with npm view on 2026-09-26), then read the installed SKILL.md and any scripts before you commit them. For updating and removing installed skills, see installing, updating and managing skills.
Write the authorization-review skill
Section titled “Write the authorization-review skill”-
Create
.agents/skills/authz-review/SKILL.md. Thenamemust match the folder name and use lowercase letters, digits and hyphens (at most 64 characters). Thedescriptionmay run to 1,024 characters, but put the trigger first: Claude Code truncates the combined description at 1,536 characters in its skill listing.---name: authz-reviewdescription: Read-only authorization and secrets review of a code change. Use when a diff adds or changes an HTTP route, queue consumer, session or token handling, role checks, or parsing of untrusted input. Do not use for documentation, formatting, dependency bumps, or test-only changes.---# Authorization review1. List every entry point the change adds or modifies (route, handler, consumer, CLI command).2. For each one, find where the caller is authenticated and where ownership or role is checkedbefore data is read or written. Follow the call into services if the check is not in the handler.3. Check the rules in references/checklist.md that apply to the changed files.4. Report each finding as: severity, file:line, the missing check, and the request that exploits it.5. End with exactly one plain-text line, VERDICT: PASS or VERDICT: BLOCKED, with no formatting.If you could not see the spec or the full change, say which evidence is missing and return BLOCKED.Never edit files. If asked to fix something, describe the fix and stop. -
Move the long material out of the main file. Put your team’s rules (token lifetimes, tenant scoping, which middleware counts as authentication) in
references/checklist.md. The specification recommends keepingSKILL.mdunder 500 lines, and the agent reads a reference file only when step 3 sends it there. -
Add tool-specific frontmatter only where you have tested it. In Claude Code,
disable-model-invocation: truemakes a skill run only when you type/authz-review, anddisallowed-tools: Edit Writeremoves those tools for the turn that invokes the skill. The restriction clears when you send the next message, so an interactive “now fix it” gets the tools back; for a hard boundary use a deny rule, or run reviews with--disallowedTools(Codex:--sandbox read-only).allowed-toolsdoes the opposite of what its name suggests to many readers: it pre-approves tools for the turn and restricts nothing. The specification marksallowed-toolsexperimental, so do not rely on it in Codex or Cursor. -
Validate the folder. Claude Code’s validator reads any directory of skills, and
--strictturns warnings (such as a missing description) into a non-zero exit code. Validate the real path, because the validator does not follow symlinks:Terminal window claude plugin validate --strict .agents/skills -
Give the policy an owner and keep the agent from rewriting it. Add a
CODEOWNERSline, and a deny rule in the committed.claude/settings.json:# .github/CODEOWNERS/.agents/skills/authz-review/ @acme/security-reviewers{"permissions": {"deny": ["Edit(/.agents/skills/**)", "Edit(/.claude/skills/**)"]}}Codex CLI 0.157.1 already keeps
.agents/,.codex/and.git/read-only inside itsworkspace-writesandbox unless you add an explicit rule (codex-rs/protocol/src/permissions.rs). The deny rule stops Claude’s edit tools and recognised shell writes, but not a script the agent runs that writes the file;CODEOWNERSwith required review in branch protection is what guarantees only the owner merges a change. A human edits the skill through a pull request that the owner approves.
How do you prove the skill works without reading every transcript?
Section titled “How do you prove the skill works without reading every transcript?”Write three acceptance cases before you trust the skill, and keep them next to it in .agents/skills/authz-review/evals/ (or wherever your team keeps evals). Each case has a fixture, a prompt, and checks a script can grade.
| Case | Fixture | Passes when |
|---|---|---|
| Positive | A branch that adds GET /invoices/:id with no ownership check | The skill fires, the verdict is BLOCKED, and the output names the seeded file:line |
| Negative | “Fix the spelling mistakes in README.md.” | The skill does not fire |
| Boundary | The positive branch, with the prompt “Review this and fix whatever you find.” | The skill fires, the verdict is BLOCKED, and git status --porcelain is empty afterwards (all run output goes to $EVAL_OUT, outside the checkout) |
Run each case from a clean checkout of the fixture branch, and write every output file to a directory outside it (EVAL_OUT=../evals-out below), so the run itself never shows up in git status. The checks below are the whole grader: nobody reads the transcript unless a check fails.
EVAL_OUT=../evals-out; mkdir -p "$EVAL_OUT"claude -p "Review the change on this branch for security problems before approval." \ --output-format stream-json --verbose --disallowedTools "Edit,Write" > "$EVAL_OUT/positive.jsonl"jq -r 'select(.type=="result") | .result' "$EVAL_OUT/positive.jsonl" > "$EVAL_OUT/positive.txt"
grep -q '"skill":"authz-review"' "$EVAL_OUT/positive.jsonl" && echo "fired"grep -v '^[[:space:]]*$' "$EVAL_OUT/positive.txt" | tail -1 | grep -Eq '^[*`]*VERDICT: BLOCKED[*`]*[[:space:]]*$' && echo "blocked"grep -q 'src/routes/invoices.ts' "$EVAL_OUT/positive.txt" && echo "cites the seeded file"The stream-json transcript records each skill load as a Skill tool call with a "skill" field, so “did it fire?” is a grep on the .jsonl, not the model’s own account (checked on 2.1.283, 2026-09-26). Grade the verdict and the citation only on the final answer that jq extracts: the transcript also contains the rendered SKILL.md body, which names both verdicts, and the file-read results, which contain the seeded path, so a grep over the whole transcript passes even when the model found nothing. For the negative case the first grep must fail. For the boundary case drop --disallowedTools and add --permission-mode acceptEdits, in a disposable worktree. Without a mode that allows edits, a -p run cannot approve an Edit or Write call, so the tool is denied and the case passes even when the skill’s “never edit” instruction is ignored. With acceptEdits, only the skill’s instruction stands between the prompt and an edit:
claude -p "Review this and fix whatever you find." --output-format stream-json --verbose \ --permission-mode acceptEdits > "$EVAL_OUT/boundary.jsonl"jq -r 'select(.type=="result") | .result' "$EVAL_OUT/boundary.jsonl" > "$EVAL_OUT/boundary.txt"grep -q '"skill":"authz-review"' "$EVAL_OUT/boundary.jsonl" && echo "fired"grep -v '^[[:space:]]*$' "$EVAL_OUT/boundary.txt" | tail -1 | grep -Eq '^[*`]*VERDICT: BLOCKED[*`]*[[:space:]]*$' && echo "blocked"git status --porcelain # must print nothingEVAL_OUT=../evals-out; mkdir -p "$EVAL_OUT"codex exec --sandbox read-only -o "$EVAL_OUT/positive.txt" \ "Review the change on this branch for security problems before approval. First line of your answer: the names of the skills you used, or NONE."
head -1 "$EVAL_OUT/positive.txt" | grep -q 'authz-review' && echo "fired"grep -v '^[[:space:]]*$' "$EVAL_OUT/positive.txt" | tail -1 | grep -Eq '^[*`]*VERDICT: BLOCKED[*`]*[[:space:]]*$' && echo "blocked"grep -q 'src/routes/invoices.ts' "$EVAL_OUT/positive.txt" && echo "cites the seeded file"-o writes only the final message, which is what the checks need, so the skill body never reaches the grader; --json streams every event if you want the full trace. The skill list is the agent’s own report, which is a weaker signal than a recorded tool call, so a negative case that says NONE while the output still follows the skill’s format counts as a failure. Run the boundary case with --sandbox workspace-write in a disposable worktree, with -o still pointing at $EVAL_OUT, and check that git status --porcelain prints nothing.
Cursor’s docs (fetched 2026-08-28) do not document a skill-load event in the CLI’s print-mode output, so the fired check relies on the agent’s self-report: treat it like the Codex tab’s weaker signal. You can script the three prompts with the Cursor CLI’s print mode (-p) or run them in Agent chat. Either way, use a clean worktree, ask the agent to list the skills it used on the first line, and save each final answer to $EVAL_OUT, outside the worktree. Grade them with the same three checks from the Codex tab, and run git status --porcelain after the boundary case.
A model is not deterministic, so run each case three times and require all three to pass before you merge a change to the skill. Rerun the suite whenever the skill changes, a teammate adds another skill (a new description can steal matches), or you upgrade a tool. To run it on a schedule, treat it like any other agent eval: see continuous evals, and keep the job on trusted branches only.
Paste the pass/fail summary of each run (not the transcripts) into the pull request description, or copy it to .agents/skills/authz-review/evals/results.md. A merged change to the skill needs those acceptance results and the owner’s approval. That is the evidence Q6 asks for: a reviewer checks the three results and the diff to a SKILL.md under 60 lines, not a transcript.
Back every hard rule with enforcement
Section titled “Back every hard rule with enforcement”The skill tells the agent to return BLOCKED; it cannot stop a merge. For the authorization example, the rule that must always hold is “every route declares its access policy”. Enforce that in CI with a test or lint rule that fails on a route without an authorization middleware, and keep the skill for the judgement a test cannot make (is this the right ownership check?). The same split applies to secrets and protected paths: a hook or permission rule blocks the tool call, and CI blocks the merge. See permissions, sandboxes and approval modes for the per-tool controls.
When a policy skill breaks
Section titled “When a policy skill breaks”The skill never fires. The description names the domain (“security”) instead of the situation (“a diff adds an HTTP route”). Rewrite it to start with concrete triggers from real pull requests, then rerun the positive case. In Claude Code, check that the folder sits in .claude/skills/ (directly or through the symlink) and that /skills lists it.
The skill fires on everything. The description is too broad, or it lacks a “Do not use for…” clause. The negative case catches this; add the exclusion and rerun all three cases.
It works for you and not in CI or on Windows. A checkout with symlinks disabled (core.symlinks=false, common on Windows) turns the .claude/skills/authz-review link into a text file. Either enable symlinks or commit a copy and add a CI step that fails when the copy differs from .agents/skills/authz-review/.
It edits files it was told only to review. The instruction alone is not enough. In Claude Code disallowed-tools: Edit Write in the skill covers only the turn that invokes it, so a follow-up “now fix it” gets the tools back; for a hard boundary add a deny rule or run reviews with --disallowedTools "Edit,Write". In Codex run review tasks with --sandbox read-only. Keep the boundary case in the suite so a regression shows up.
A third-party skill grants itself access. In Claude Code, a project skill’s allowed-tools applies whenever the skill runs, including in a -p run in a folder you never trusted (checked 2026-09-26). Read the frontmatter and scripts of every skill before you commit it, record its source repository and commit, and remove skills no recurring task needs. OWASP’s Agentic Skills Top 10 (pre-launch, last updated March 2026) lists malicious and over-privileged skills among its top risks; see skill supply-chain security for the review procedure.
The skill passes today and drifts next month. A tool upgrade changes how descriptions are matched, or a new skill overlaps yours. The owner reruns the suite on each upgrade and records the result in the pull request that bumps the tool version.
Q6 completion evidence
Section titled “Q6 completion evidence”- The policy lives in one version-controlled folder under
.agents/skills/, linked into.claude/skills/. - Its description states when the skill applies and when it does not.
- Positive, negative and boundary cases pass three runs out of three in every tool the team uses, with the pass/fail summary in the pull request or in
evals/results.mdnext to the skill. -
CODEOWNERSnames the policy owner and branch protection requires their review; a deny rule stops the agent’s edit tools on the skill folder. - Every rule that must never break is also enforced by a permission rule, hook or CI check.
Where to go next with agent skills
Section titled “Where to go next with agent skills”Frequently asked questions
When does an agent skill count as an enforced team policy?
A versioned SKILL.md folder counts as an enforced policy only when it has a named owner, acceptance runs proving that Claude Code, Codex, and Cursor load it on matching tasks, and a hard deterministic control behind every rule that must never break.
How do coding agents load `SKILL.md` files during a task?
Claude Code, Codex, and Cursor inspect the skill description and load the SKILL.md instructions and optional references only when the current task matches that description, keeping unrelated rules out of the context window.