Skill Authoring skill for Codex
From repeated prompt to a skill that triggers. Codex starts from real requests and a failure case, decides whether the guidance belongs in a skill at all, writes a precise name and description, and builds a body of steps, decisions, gotchas, verification and an output template. A bundled linter checks names, description length, YAML quoting, links and unreferenced files, and the skill is tried both with $name and implicitly before it ships.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- skill-authoring Installs to ~/.agents/skills/skill-authoring/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install skill-authoring The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project skill-authoring Installs to .agents/skills/skill-authoring/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/skill-authoring.zipunzip -o skill-authoring.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) skill-authoring PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $skill-authoring, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes a new Codex skill end to end, deciding whether the workflow belongs in a skill, AGENTS.md or a plugin, choosing the install location (.agents/skills, ~/.agents/skills), drafting SKILL.md frontmatter and a procedural body with decisions, gotchas and verification, adding references or scripts only where they pay off, and testing explicit $skill and implicit invocation. Includes a linter for frontmatter, links and bundled files. Use when a user asks to create a skill, turn a repeated prompt or runbook into a skill, or review a draft SKILL.md.
Use it when
- You paste the same long prompt or runbook into Codex repeatedly
- A team workflow should be checked into .agents/skills
- A draft SKILL.md needs review before sharing
- Unsure whether guidance belongs in AGENTS.md, a skill or a plugin
Use something else when
- The skill exists but triggers too often or never: use skill-description-tuning
- You need scripts, references or distribution details: use skill-scripts-packaging
- You want automated trigger and output tests: use skill-evals
How to check the result
Run scripts/check_skill.py on the folder with no errors, then confirm the skill loads for target prompts, stays unloaded for near-miss prompts, and produces the output template when invoked by name.
The mistake it prevents
Writing a general essay about the topic instead of the specific procedure, thresholds and traps Codex would otherwise get wrong.
A finished skill does one job, triggers on the requests that need it and stays quiet otherwise, and makes Codex do something it would not reliably do on its own: follow a specific procedure, apply thresholds, avoid known traps, and prove the result. It passes the linter, and it has been tried on real prompts, both by name and implicitly.
Is a skill the right container?
| The guidance is… | Put it in |
|---|---|
| Always true for this repository (commands, conventions, boundaries) | AGENTS.md |
| A reusable procedure used only for some tasks | A skill |
| Several skills plus MCP servers or connectors for others to install | A plugin wrapping skills (see codex-plugin-packaging) |
| One fixed prompt you paste often, no branching | A skill with allow_implicit_invocation: false (custom prompts in ~/.codex/prompts are deprecated in favour of skills) |
| Access to an external system | An MCP server, optionally with a skill that teaches its use |
Where skills live
| Scope | Path | Use for |
|---|---|---|
| Repo | .agents/skills/<name>/ in the cwd, any parent up to the repo root | Team workflows tied to this code; reviewed in PRs |
| User | ~/.agents/skills/<name>/ | Personal workflows across repos |
| Admin | /etc/codex/skills/<name>/ | Machine or container defaults, automation images |
| System | Bundled with Codex (skill-creator, skill-installer, plan, …) | Not editable |
Symlinked skill folders are followed. Two skills with the same name are not merged;
both appear in selectors, which confuses implicit matching. Codex picks up changes
automatically; restart if a new skill does not appear. Skills in the repository are
available in Codex Cloud tasks; personal skills are not synced there.
Workflow
- [ ] 1. Collect 3+ real requests and one worked example of the job done well
- [ ] 2. Write name and description (trigger surface)
- [ ] 3. Write the body: standard, workflow, decisions, gotchas, verification, output
- [ ] 4. Move bulk into references/ and deterministic work into scripts/
- [ ] 5. Lint
- [ ] 6. Test explicit and implicit invocation; iterate
1. Gather evidence first
Ask for, or find in history: the prompts people actually type, the last time this task
went wrong and why, and a good final artifact (PR, report, config). A skill written
without failure cases is a generic essay. $skill-creator can interview you and scaffold
the folder; edit its output with the rules below.
2. Frontmatter
---
name: release-notes
description: Drafts user-facing release notes from merged PRs and conventional commits between two git tags, grouping by feature, fix and breaking change and linking PR numbers. Use when asked for release notes, a changelog entry, or "what shipped since v2.3", or when preparing a GitHub release.
---
name: 1-64 chars, lowercase letters, digits and single hyphens, equal to the folder.description(max 1024 chars): the only text Codex sees before deciding to load the skill, and it may be shortened when many skills are installed. Sentence one: what it does with concrete nouns. Then “Use when …” with real trigger phrases and files. Third person, no markup.- Optional spec keys:
license,compatibility(environment needs, max 500 chars),metadata(string map),allowed-tools(experimental). Codex readsnameanddescription; keep other keys minimal for portability.
For trigger tuning, use skill-description-tuning.
3. Body
Write imperative instructions to the agent, in the order a senior practitioner works:
# Release notes
Done means: every merged PR since the last tag is either in the notes or listed as
internal, breaking changes come first with migration steps, and links resolve.
## Workflow
1. Read AGENTS.md for the release process and tag format.
2. Find the range: `git describe --tags --abbrev=0`, then `git log <tag>..HEAD --merges`.
3. ...
## Decisions
| Commit type | Section |
|---|---|
| feat | Features |
| fix | Fixes |
| BREAKING CHANGE footer | Breaking changes (first) |
## Gotchas
- Squash-merged PRs have no merge commit; use `gh pr list --state merged --search "merged:>DATE"`.
## Verify
- Count PRs in range vs entries + internal list; they must match.
## Output
<template>
Rules that separate good skills from filler:
- Open with the standard of done, not an introduction.
- Prefer one recommended path with an escape hatch over a menu of equal options.
- Put exact commands, flags, file paths and thresholds. If unsure a flag exists, describe
the approach and point to
--help. - State what needs network or broader sandbox access, since Codex often runs with network off. Never instruct Codex to bypass approvals.
- Require explicit user confirmation before destructive or external actions (force push, deploys, sending messages, payments, deleting data).
- Tell Codex to read
AGENTS.mdwhen project conventions matter. - Target 100-300 lines; hard ceiling around 500. Spend lines on the parts Codex gets wrong, not on what it already knows.
4. Supporting files
references/<topic>.md: depth needed only in some branches. Link it from the body with WHEN to read it, for example “read the squash-merge reference only if the repo squash-merges”. Keep references one level deep.scripts/: deterministic steps (parsing, validation, measuring). Standard library, clear output, non-zero exit on failure, documented with the exact command.assets/: templates and static files the output is built from.agents/openai.yaml: optional UI metadata, invocation policy and tool dependencies.
Details and testing of these files: skill-scripts-packaging.
5. Lint
python3 scripts/check_skill.py .agents/skills/release-notes
Errors: bad name, name/folder mismatch, description missing or over 1024 chars, YAML values that need quoting, links to files that do not exist. Warnings: weak description, non-spec keys, files never mentioned in SKILL.md, scripts without a shebang, body over 500 lines.
6. Test
- Explicit: in the CLI type
$release-notes(or pick it from/skills) with a real request. Watch whether Codex follows the steps in order and produces the template. - Implicit: new session, type the realistic prompts from step 1 without naming the skill. It should load. Then try two near-miss prompts that should NOT load it.
- Headless check of the same:
Inspect the events for a read of the skill’scodex exec --json "what shipped since v2.3?" > run.jsonlSKILL.md. - Fix the cause, not the symptom: wrong trigger means description work; skipped steps mean the body is too long or vague; wrong commands mean missing specifics.
For repeatable trigger and output tests, use skill-evals.
Gotchas
- A description that lists every possible topic triggers everywhere and wastes the
shared skills budget (about 2% of context by default, configurable with
skills.max_context_tokensup to 10000 tokens). - Repo skills from a cloned untrusted repository are instructions from that repo’s author. Read them before use.
- Disable a skill without deleting it with
[[skills.config]](path = ".../SKILL.md",enabled = false) in~/.codex/config.toml. - Do not put secrets, personal tokens or internal hostnames in a skill you share.
Output contract
**Skill:** .agents/skills/release-notes (SKILL.md 182 lines, one helper script)
**Lint:** check_skill.py ok, 0 warnings
**Trigger test:** 4/4 target prompts loaded it; 0/2 near-miss prompts did
**Explicit run:** produced notes for v2.3..HEAD; PR count matched What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Skill Authoring"
short_description: "From repeated prompt to a skill that triggers"
default_prompt: "$skill-authoring Turn our release checklist into a Codex skill" scripts/check_skill.py
#!/usr/bin/env python3
"""Lint an Agent Skill folder before installing or sharing it with Codex.
Checks the SKILL.md frontmatter against the Agent Skills specification
(name rules, description length, known keys), the body size, that every
relative file the body links to exists, that every bundled file is
mentioned somewhere in SKILL.md, and that scripts have a shebang.
Usage:
python3 scripts/check_skill.py PATH/TO/skill-folder [PATH ...]
python3 scripts/check_skill.py ~/.agents/skills/* # many at once
python3 scripts/check_skill.py my-skill --ignore notes.txt # skip a file in the unreferenced-file check
Exit codes: 0 no errors (warnings allowed), 1 errors found, 2 usage error.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
KNOWN_KEYS = {"name", "description", "license", "compatibility", "metadata", "allowed-tools"}
NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
LINK_RE = re.compile(r"\]\(([^)#\s]+)(?:#[^)]*)?\)")
PATH_RE = re.compile(r"(?<![\w/.-])((?:scripts|references|assets|agents)/[\w./-]+)")
def parse_frontmatter(text: str) -> tuple[dict[str, str], str, list[str]]:
errors: list[str] = []
if not text.startswith("---\n"):
return {}, text, ["SKILL.md must start with '---' on line 1"]
end = text.find("\n---", 4)
if end == -1:
return {}, text, ["frontmatter is not closed with '---'"]
block, body = text[4:end], text[end + 4:].lstrip("\n")
data: dict[str, str] = {}
key = None
for line in block.splitlines():
if not line.strip() or line.lstrip().startswith("#"):
continue
m = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", line)
if m and not line.startswith((" ", "\t")):
key, value = m.group(1), m.group(2).strip()
if value in ("|", ">", "|-", ">-"):
value = ""
elif len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
value = value[1:-1]
elif re.search(r":\s", value) or re.search(r"\s#", value):
errors.append(f"unquoted value for '{key}' contains ': ' or ' #'; wrap it in double quotes")
data[key] = value
elif key is not None:
data[key] = (data[key] + " " + line.strip()).strip()
return data, body, errors
def check(folder: Path, ignore: set[str]) -> tuple[list[str], list[str]]:
errors: list[str] = []
warnings: list[str] = []
skill = folder / "SKILL.md"
if not skill.is_file():
return [f"{skill} not found"], warnings
text = skill.read_text(encoding="utf-8")
meta, body, fm_errors = parse_frontmatter(text)
errors += fm_errors
name = meta.get("name", "")
if not name:
errors.append("missing 'name'")
else:
if len(name) > 64 or not NAME_RE.match(name):
errors.append(f"name '{name}' must be 1-64 chars of a-z, 0-9 and single hyphens, not starting or ending with '-'")
if name != folder.name:
errors.append(f"name '{name}' must equal the folder name '{folder.name}'")
desc = meta.get("description", "")
if not desc:
errors.append("missing 'description'")
else:
if len(desc) > 1024:
errors.append(f"description is {len(desc)} chars (max 1024)")
if len(desc) < 120:
warnings.append(f"description is only {len(desc)} chars; say what it does and when to use it")
if "<" in desc and ">" in desc:
warnings.append("description contains angle brackets; some hosts reject XML-like text")
if not re.search(r"\b(use when|use for|use if|when )", desc, re.I):
warnings.append("description has no trigger clause such as 'Use when ...'")
for key in meta:
if key not in KNOWN_KEYS:
warnings.append(f"frontmatter key '{key}' is not in the Agent Skills spec; other hosts may ignore or reject it")
if "compatibility" in meta and len(meta["compatibility"]) > 500:
errors.append("compatibility is longer than 500 chars")
lines = text.count("\n") + 1
if lines > 500:
warnings.append(f"SKILL.md has {lines} lines; move detail into references/")
if len(body.strip()) < 200:
warnings.append("body is very short; is the procedure really in SKILL.md?")
# Outside fenced code blocks (which hold illustrative examples), markdown links
# must resolve (error); bare paths should resolve too but only warn.
prose = re.sub(r"```.*?```", "", body, flags=re.S)
links = {t for t in LINK_RE.findall(prose) if not re.match(r"^[a-z]+:", t) and not t.startswith("/")}
for target in sorted(links):
if not (folder / target).exists():
errors.append(f"link to '{target}' does not resolve inside the skill folder")
for target in sorted({t.rstrip(".,;:`") for t in PATH_RE.findall(prose)} - links - {"agents/openai.yaml"}):
if not (folder / target).exists():
warnings.append(f"mentions '{target}', which is not in the skill folder")
for path in sorted(folder.rglob("*")):
if path.is_dir() or path.name == "SKILL.md" or "__pycache__" in path.parts or path.name.startswith("."):
continue
rel = path.relative_to(folder).as_posix()
if rel == "agents/openai.yaml" or path.name in ignore or path.name.upper().startswith("LICENSE"):
continue
if rel not in text and path.name not in text:
warnings.append(f"{rel} is never mentioned in SKILL.md, so the agent will not know to use it")
if rel.startswith("scripts/") and path.suffix in {".py", ".sh", ".mjs", ".js"}:
first = path.read_text(encoding="utf-8", errors="replace").splitlines()[:1]
if not first or not first[0].startswith("#!"):
warnings.append(f"{rel} has no shebang line")
return errors, warnings
def main(argv: list[str]) -> int:
if not argv or argv[0] in ("-h", "--help"):
print(__doc__)
return 2 if not argv else 0
ignore: set[str] = set()
folders: list[str] = []
args = iter(argv)
for arg in args:
if arg == "--ignore":
ignore.add(next(args, ""))
else:
folders.append(arg)
failed = False
for arg in folders:
folder = Path(arg).expanduser().resolve()
if not folder.is_dir():
print(f"{arg}: not a directory", file=sys.stderr)
failed = True
continue
errors, warnings = check(folder, ignore)
status = "FAIL" if errors else "ok"
print(f"{status} {folder.name}")
for e in errors:
print(f" error: {e}")
for w in warnings:
print(f" warn: {w}")
failed = failed or bool(errors)
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.