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.

  • Codex Mastery
  • $skill-authoring
  • 180 lines
  • 3 files
  • ~1,900 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- skill-authoring

Installs to ~/.agents/skills/skill-authoring/ for every project on this machine.

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

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.

~/.agents/skills/skill-authoring/SKILL.md

--- name: skill-authoring description: 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… license: MIT ---

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 tasksA skill
Several skills plus MCP servers or connectors for others to installA plugin wrapping skills (see codex-plugin-packaging)
One fixed prompt you paste often, no branchingA skill with allow_implicit_invocation: false (custom prompts in ~/.codex/prompts are deprecated in favour of skills)
Access to an external systemAn MCP server, optionally with a skill that teaches its use

Where skills live

ScopePathUse for
Repo.agents/skills/<name>/ in the cwd, any parent up to the repo rootTeam 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
SystemBundled 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 reads name and description; 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.md when 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

  1. 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.
  2. 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.
  3. Headless check of the same:
    codex exec --json "what shipped since v2.3?" > run.jsonl
    Inspect the events for a read of the skill’s SKILL.md.
  4. 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_tokens up 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.yamlgenerated from this page

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.py157 lines, 6.7 KB
#!/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.

esc