Agent Skills now belongs to the Linux Foundation, not to one AI company. Here's what that changes, where each agent looks for skills, and a complete writing-quality skill you can drop into Claude Code, Codex and Gemini CLI today.
TL;DR: On October 22, 2026, Anthropic donated the Agent Skills standard to the Agentic AI Foundation under the Linux Foundation. The SKILL.md format didn't change, which is the point: the same skill folder works across Claude Code, OpenAI Codex, Gemini CLI and a long list of other agents. In this post I build a writing-quality skill with a dependency-free Python checker, test it on real drafts, and show the folder layout that makes all three agents find it.
Anthropic introduced Agent Skills as an open standard in December 2025. On October 22, 2026, it donated the standard to the Agentic AI Foundation (AAIF), the Linux Foundation project that also hosts MCP. The donation covers the name, the logo, the GitHub organization and the agentskills.io domain.
What that means in practice:
SKILL.md. name and description are still the only required fields. Your existing skills keep working.
Anthropic says more than 70 agent platforms have integrated the format, with about 1 million skills published and about 280 million installs. Those are vendor-reported numbers, but the practical takeaway is clear: if you write a skill once against the spec, you're no longer writing it for one company's agent.
The format is shared. The folders are not, at least not completely. Here's where each of the three agents in this post discovers skills, taken from each vendor's own docs:
| Agent | Project skills | Personal skills | How it activates |
|---|---|---|---|
| Claude Code | .claude/skills/<name>/ |
~/.claude/skills/<name>/ |
Automatically when the description matches, or /skill-name |
| OpenAI Codex | .agents/skills/<name>/ (from your working directory up to the repo root) |
~/.agents/skills/<name>/ |
Automatically, or $skill-name //skills |
| Gemini CLI | .gemini/skills/<name>/ or the.agents/skills/ alias |
~/.gemini/skills/ or~/.agents/skills/ |
Calls activate_skill after you approve a consent prompt |
Sources: Claude Code skills docs, Codex skills docs, Gemini CLI skills docs.
The good news: .agents/skills/ is already shared by Codex and Gemini CLI. Claude Code reads .claude/skills/. One symlink covers the gap, which I'll show in section 7.
All three load skills the same way under the hood. At startup they read only each skill's name and description (about 100 tokens). The full SKILL.md is loaded only when a task matches, and bundled files like scripts are read only when the instructions point to them. That progressive is why a good description matters more than anything else in the file.
Writing quality is a good fit for a skill because it needs two things a plain prompt handles badly:
The skill is called writing-quality. It runs a local checker on a draft and reports eight things:
The script uses only the Python standard library, so it runs anywhere an agent can run Python. No API key, no network, no pip install.
Here's the folder:
writing-quality/
βββ SKILL.md
βββ references/
β βββ CHECKS.md
βββ scripts/
βββ check_writing.py
---
name: writing-quality
description: Reviews and tightens prose drafts (blog posts, docs, newsletters, emails, essays) for clarity, rhythm and filler. Runs a local checker that measures sentence-length variety, readability, passive voice, filler phrases, overused AI-style wording and repeated openers, then fixes the flagged spots without changing meaning. Use when the user asks to proofread, polish, edit, tighten or improve the writing quality of a draft, or asks why a draft sounds robotic or generic.
license: MIT
compatibility: Requires Python 3.9+ (standard library only). No network access needed.
metadata:
author: mohab-abdelkarim
version: "1.0.0"
---
Use this skill to review a prose draft and improve it with evidence, not guesswork.
## Workflow
1. Save the draft to a file if it only exists in the conversation (for example `draft.md`).
2. Run the checker that ships with this skill: `python3 <skill-folder>/scripts/check_writing.py draft.md`. The script path is relative to this SKILL.md file. Add `--json` for machine-readable output, or `--strict` to exit with code 1 when any check fails (useful in CI).
3. Read the report. Each check prints PASS or WARN with the exact sentences or phrases involved.
4. Fix only what the report flags:
- Split or vary sentences listed under "Longest sentences" when the length check warns.
- Rewrite sentences listed under "Passive voice" into active voice where the actor matters.
- Delete or replace every phrase listed under "Filler phrases" and "Overused AI-style wording".
- Reword repeated sentence openers so consecutive sentences start differently.
5. Re-run the checker on the edited draft and repeat until every check passes or the remaining warnings are deliberate.
6. Show the user a short summary: which checks failed before, what you changed, and the final report.
## Rules
- Never change facts, numbers, names, quotes or the author's claims. This skill edits style, not substance.
- Keep the author's voice. Prefer the smallest edit that clears a warning.
- Do not add new sections, headings or content the author didn't write.
- If a warning is intentional (for example a long sentence in a legal clause), leave it and say why.
## Reference
See [references/CHECKS.md](references/CHECKS.md) for what each check measures, its threshold, and how to fix it.
A few choices worth explaining:
license, compatibility and metadata are all part of the open spec, so nothing here is Claude-only or Codex-only.references/CHECKS.md holds the thresholds and fixes, and the agent reads it only when it needs to. That keeps the main file short, which the spec recommends (under 500 lines and roughly 5,000 tokens).
Here's the reference file:
| Check | What it measures | Warns when | How to fix |
|---|---|---|---|
| Sentence variety | Standard deviation of sentence length (words) | Below 5 words | Mix short sentences with longer ones; split the longest sentences |
| Long sentences | Sentences over 35 words | Any found | Split at a conjunction or move a clause into its own sentence |
| Readability | Flesch Reading Ease (approximate syllable count) | Below 50 | Use shorter words and sentences |
| Passive voice | "be" verb followed by a past participle (heuristic) | Over 15% of sentences | Name the actor and make it the subject |
| Filler phrases | Phrases like "it is important to note that", "in order to" | Any found | Delete them or use the short form ("to") |
| AI-style wording | Overused words and phrases common in generated text ("delve", "tapestry", "in today's fast-paced world") | Any found | Replace with a plain, specific word |
| Repeated openers | The same first word in 3+ consecutive sentences | Any run found | Vary how the sentences start |
| Em dashes | Count of em dashes | More than 2 per 500 words | Use commas, colons, parentheses or a new sentence |
All checks are heuristics. They point at places worth a second look; they do not prove anything about who wrote the text.
Save this as scripts/check_writing.py:
#!/usr/bin/env python3
"""Writing-quality checker for the writing-quality Agent Skill.
Standard library only. Usage:
python3 check_writing.py draft.md [--json] [--strict]
cat draft.md | python3 check_writing.py - [--json]
"""
import argparse
import json
import re
import statistics
import sys
FILLER = [
"it is important to note that", "it's important to note that", "it is worth noting that",
"it's worth noting that", "in order to", "the fact that", "at the end of the day",
"needless to say", "for all intents and purposes", "due to the fact that", "in today's world",
"when it comes to", "a wide range of", "a variety of", "it goes without saying",
"plays a crucial role", "plays an essential role", "plays a vital role",
]
AI_STYLE = [
"delve", "delves", "delving", "tapestry", "testament to", "in today's fast-paced",
"in the ever-evolving", "ever-evolving landscape", "navigate the complexities", "unlock the potential",
"unlock the power", "game-changer", "game changer", "seamlessly", "seamless", "leverage",
"harness the power", "elevate your", "embark on", "a myriad of", "robust", "pivotal",
"realm", "foster", "furthermore", "moreover", "in conclusion", "additionally",
]
BE_VERBS = r"(?:am|is|are|was|were|be|been|being)"
IRREGULAR_PP = (
"built|done|given|made|seen|shown|taken|written|known|found|kept|left|held|brought|bought|"
"taught|thought|told|sold|sent|spent|paid|put|set|run|won|chosen|driven|eaten|fallen|forgotten|"
"gotten|hidden|ridden|risen|spoken|stolen|sworn|torn|worn|broken|frozen"
)
PASSIVE_RE = re.compile(rf"\b{BE_VERBS}\s+(?:\w+ly\s+)?(?:\w+ed|{IRREGULAR_PP})\b", re.I)
def strip_markdown(text):
text = re.sub(r"```
.*?
```", " ", text, flags=re.S) # code blocks
text = re.sub(r"`[^`]*`", " ", text) # inline code
text = re.sub(r"!\[[^\]]*\]\([^)]*\)", " ", text) # images
text = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", text) # links -> text
text = re.sub(r"^\s{0,3}#{1,6}\s+.*$", " ", text, flags=re.M) # headings
text = re.sub(r"^\s*[-*+]\s+", "", text, flags=re.M) # bullets
text = re.sub(r"[*_]{1,3}", "", text) # emphasis
return text
def split_sentences(text):
text = re.sub(r"\s+", " ", text).strip()
parts = re.split(r"(?<=[.!?])[\"')\]]*\s+(?=[A-Z0-9\"'(])", text)
return [p.strip() for p in parts if len(p.split()) >= 3]
def syllables(word):
word = re.sub(r"[^a-z]", "", word.lower())
if not word:
return 0
groups = re.findall(r"[aeiouy]+", word)
count = len(groups)
if word.endswith("e") and not word.endswith(("le", "ee")) and count > 1:
count -= 1
return max(count, 1)
def find_phrases(text, phrases):
found = {}
low = text.lower().replace("\u2019", "'") # curly apostrophe
for p in phrases:
n = len(re.findall(rf"\b{re.escape(p)}\b", low))
if n:
found[p] = n
return found
def analyze(raw):
text = strip_markdown(raw)
sentences = split_sentences(text)
words = re.findall(r"[A-Za-z0-9']+", text)
if not sentences or not words:
raise ValueError("No prose found. Pass a file with at least a few full sentences.")
lengths = [len(s.split()) for s in sentences]
syl = sum(syllables(w) for w in words)
flesch = 206.835 - 1.015 * (len(words) / len(sentences)) - 84.6 * (syl / len(words))
passive = [s for s in sentences if PASSIVE_RE.search(s)]
openers = [re.sub(r"[^a-z]", "", s.split()[0].lower()) for s in sentences]
runs, i = [], 0
while i < len(openers):
j = i
while j + 1 < len(openers) and openers[j + 1] == openers[i] and openers[i]:
j += 1
if j - i + 1 >= 3:
runs.append({"word": openers[i], "count": j - i + 1})
i = j + 1
em = raw.count("\u2014") # em dash
em_limit = max(2, round(2 * len(words) / 500))
stdev = statistics.pstdev(lengths) if len(lengths) > 1 else 0.0
long_sentences = [s for s, n in zip(sentences, lengths) if n > 35]
filler = find_phrases(text, FILLER)
ai_style = find_phrases(text, AI_STYLE)
passive_share = len(passive) / len(sentences)
checks = [
{"check": "Sentence variety", "value": round(stdev, 1), "pass": stdev >= 5,
"detail": f"std dev {stdev:.1f} words (want 5+), average {statistics.mean(lengths):.1f} words"},
{"check": "Long sentences", "value": len(long_sentences), "pass": not long_sentences,
"items": long_sentences[:5]},
{"check": "Readability", "value": round(flesch, 1), "pass": flesch >= 50,
"detail": f"Flesch Reading Ease {flesch:.1f} (want 50+; higher is easier)"},
{"check": "Passive voice", "value": round(passive_share * 100), "pass": passive_share <= 0.15,
"detail": f"{len(passive)} of {len(sentences)} sentences ({passive_share:.0%}, want 15% or less)",
"items": passive[:5]},
{"check": "Filler phrases", "value": sum(filler.values()), "pass": not filler, "items": filler},
{"check": "AI-style wording", "value": sum(ai_style.values()), "pass": not ai_style, "items": ai_style},
{"check": "Repeated openers", "value": len(runs), "pass": not runs, "items": runs},
{"check": "Em dashes", "value": em, "pass": em <= em_limit, "detail": f"{em} found (limit {em_limit} for this length)"},
]
longest = sorted(zip(lengths, sentences), reverse=True)[:3]
return {
"words": len(words),
"sentences": len(sentences),
"passed": sum(c["pass"] for c in checks),
"total": len(checks),
"checks": checks,
"longest_sentences": [{"words": n, "sentence": s} for n, s in longest],
}
def shorten(text, limit):
if len(text) <= limit:
return text
return text[:limit].rsplit(" ", 1)[0] + " ..."
def print_report(r):
print(f"Writing-quality report: {r['words']} words, {r['sentences']} sentences")
print(f"Passed {r['passed']} of {r['total']} checks\n")
for c in r["checks"]:
status = "PASS" if c["pass"] else "WARN"
line = f"[{status}] {c['check']}"
if c.get("detail"):
line += f": {c['detail']}"
elif not c["pass"]:
line += f": {c['value']} found"
print(line)
if not c["pass"] and c.get("items"):
items = c["items"]
if isinstance(items, dict):
for k, v in items.items():
print(f" - \"{k}\" x{v}")
else:
for it in items:
text = it if isinstance(it, str) else f"{it['count']} sentences in a row start with \"{it['word']}\""
print(f" - {shorten(text, 160)}")
print("\nLongest sentences:")
for s in r["longest_sentences"]:
print(f" ({s['words']} words) {shorten(s['sentence'], 140)}")
def main():
ap = argparse.ArgumentParser(description="Check a prose draft for clarity, rhythm and filler.")
ap.add_argument("path", help="Markdown or text file, or - for stdin")
ap.add_argument("--json", action="store_true", help="print JSON instead of a text report")
ap.add_argument("--strict", action="store_true", help="exit 1 if any check fails")
a = ap.parse_args()
try:
raw = sys.stdin.read() if a.path == "-" else open(a.path, encoding="utf-8").read()
report = analyze(raw)
except (OSError, ValueError) as e:
print(f"error: {e}", file=sys.stderr)
return 2
if a.json:
print(json.dumps(report, indent=2))
else:
print_report(report)
return 1 if a.strict and report["passed"] < report["total"] else 0
if __name__ == "__main__":
sys.exit(main())
It takes a file path or - for stdin, strips Markdown (code blocks, headings, links and emphasis) so it only measures prose, and supports --json for agents and --strict for CI.
I ran the checker on two kinds of text:
Here's the checker's real output on the ChatGPT academic paragraph:
The checker passed 5 of 8 checks on the ChatGPT paragraph. It flagged flat sentence rhythm, dense wording and one filler phrase.
And here's one of the student essays:
Writing-quality report: 398 words, 22 sentences
Passed 6 of 8 checks
[PASS] Sentence variety: std dev 7.6 words (want 5+), average 17.9 words
[WARN] Long sentences: 1 found
- For example, The time I took an online class for art, it was terrible, either I didn't know how to transfer my painting to the computer or I just didn't know ...
[PASS] Readability: Flesch Reading Ease 59.4 (want 50+; higher is easier)
[PASS] Passive voice: 0 of 22 sentences (0%, want 15% or less)
[PASS] Filler phrases
[WARN] AI-style wording: 1 found
- "in conclusion" x1
[PASS] Repeated openers
[PASS] Em dashes: 0 found (limit 2 for this length)
Longest sentences:
(36 words) For example, The time I took an online class for art, it was terrible, either I didn't know how to transfer my painting to the computer or ...
(31 words) Students some times need an actual teacher showing or explaining to them face to face content, others take extra-curricular activities, and ...
(27 words) Take for example my cousin, She's taking online classes but has a difficult time learning the material because she doesn't understand the ...
The most useful signal across all twelve texts was sentence variety:
| Text | Sentence variety (std dev, words) | Flesch Reading Ease | Checks passed |
|---|---|---|---|
| ChatGPT essay (distance learning) | 4.1 | 32.7 | 5 of 8 |
| ChatGPT essay ("student" style) | 5.3 | 62.7 | 7 of 8 |
| ChatGPT essay (technology) | 6.2 | 42.7 | 6 of 8 |
| ChatGPT academic paragraph | 3.0 | 6.3 | 5 of 8 |
| ChatGPT blog intro | 3.8 | 18.2 | 6 of 8 |
| ChatGPT LinkedIn post | 5.2 | 44.1 | 7 of 8 |
| Student essay H1 | 7.6 | 59.4 | 6 of 8 |
| Student essay H2 | 7.6 | 62.0 | 7 of 8 |
| Student essay H3 | 12.0 | 68.9 | 6 of 8 |
| Student essay H4 | 18.2 | 62.8 | 7 of 8 |
| Student essay H5 | 7.8 | 75.9 | 6 of 8 |
| Student essay H6 | 3.5 | 76.7 | 7 of 8 |
Each dot is one text. Five of the six student essays cleared the variety check, while half of the ChatGPT drafts fell below it.
The ChatGPT drafts had a median standard deviation of 4.65 words. The student essays came in at 7.7. In plain terms, the AI drafts kept their sentences close to the same length, while most of the students mixed short and long ones.
Two honest caveats:
I also tested the edge cases:
python3 .claude/skills/writing-quality/scripts/check_writing.py draft.md --strict
echo $? # 1
python3 .agents/skills/writing-quality/scripts/check_writing.py draft.md --json
echo "hello" | python3 .agents/skills/writing-quality/scripts/check_writing.py -
Because Codex and Gemini CLI both read .agents/skills/, put the real folder there and point Claude Code at it with a symlink:
mkdir -p .agents/skills .claude/skills
cp -r writing-quality .agents/skills/
ln -s ../../.agents/skills/writing-quality .claude/skills/writing-quality
The result:
your-repo/
βββ .agents/
β βββ skills/
β βββ writing-quality/ <- Codex and Gemini CLI read this
β βββ SKILL.md
β βββ references/CHECKS.md
β βββ scripts/check_writing.py
βββ .claude/
βββ skills/
βββ writing-quality -> ../../.agents/skills/writing-quality <- Claude Code reads this
Commit both folders and everyone on the team gets the skill, whichever agent they use. The script runs the same from either path. I checked both.
If you'd rather not manage folders by hand, the open-source skills CLI installs a skill for several agents at once and knows each agent's paths:
npx skills add ./writing-quality -a claude-code -a codex -a gemini-cli
Per its docs, it puts project skills in .claude/skills/ for Claude Code and .agents/skills/ for Codex and Gemini CLI. The -g flag installs globally instead.
To use it, ask any of the three agents something like "proofread draft.md and tighten it." The description matches, the agent loads the skill, runs the checker, edits the flagged spots and re-runs the checker. You can also call it directly: /writing-quality in Claude Code or $writing-quality in Codex.
Each vendor supports optional extensions on top of the spec. They're useful, but they're where portability quietly breaks, so keep them out of the core skill unless you need them.
Claude Code adds frontmatter fields like disable-model-invocation (only you can trigger the skill), context: fork (run it in a subagent), paths (activate only for certain files) and argument-hint. They're Claude Code features, so don't count on them doing anything in Codex or Gemini CLI.
Codex reads an optional agents/openai.yaml inside the skill folder for display names, icons, tool dependencies, and allow_implicit_invocation (set it to false if you only want the skill when someone calls it by name).
Gemini CLI asks for consent before activating any skill and shows the folder it will access. There's nothing to configure, but it's worth knowing your teammates will see that prompt.
allowed-tools is in the spec but marked experimental, and support varies. For a skill that only runs a local Python script, you don't need it.
My rule: write the core skill against the spec only, test it, then add vendor extras in separate files or fields that degrade gracefully.
The checker in this post measures and flags. It doesn't rewrite. If you want skills that handle full writing workflows, such as SEO articles, newsletters, product descriptions, docs or brand voice, Walter Skills is an MIT-licensed set of 12 skills built for that, from the team behind the Walter Writes AI authenticity platform. They install with the same CLI:
npx skills add walterwritesai/walter-skills
Or a single skill:
npx skills add walterwritesai/walter-skills --skill walter-seo-writer
They pair well with a checker like this one: let a workflow skill produce the draft, then run writing-quality as the final pass. Note that the humanization and AI-detection steps in Walter Skills rely on Walter's MCP server, so you'll want that connected for the full workflow.
Agent Skills moving to the Linux Foundation doesn't change a single line of your SKILL.md. What it changes is the bet you're making when you write one. A skill is now a portable unit of know-how, not a feature of one vendor's agent.
The practical recipe is simple:
references/..agents/skills/ and symlink it into .claude/skills/.
The writing-quality skill in this post follows all four. Copy it, change the thresholds and phrase lists to match your style guide, and it'll work the same in Claude Code, Codex and Gemini CLI.
What is the Agent Skills standard?
It's an open format for packaging instructions, scripts and resources that AI agents load on demand. A skill is a folder with a SKILL.md file that has name and description frontmatter plus Markdown instructions. The spec lives at agentskills.io.
Who owns Agent Skills now?
Since October 22, 2026, the Agentic AI Foundation under the Linux Foundation. Anthropic donated it, and engineers from Anthropic, Google, OpenAI and other companies maintain it.
Do Claude Code, Codex and Gemini CLI use the same skill folder?
Codex and Gemini CLI both read .agents/skills/. Claude Code reads .claude/skills/. Keep the skill in .agents/skills/ and add a symlink in .claude/skills/, or use npx skills add with each agent flag.
Can a skill include code?
Yes. Put scripts in a scripts/ folder and tell the agent when to run them in SKILL.md. Keep them self-contained and document any dependencies in the compatibility field.
How do I validate a skill?
The spec's reference library includes a validator: skills-ref validate ./my-skill. It checks the frontmatter and naming rules, including that name matches the folder name.
Is the writing-quality checker an AI detector?
No. It measures style patterns like uniform sentence length and filler phrases so you can edit them. Real human writing can trip the same checks, so never use it to judge who wrote something.
Why does my skill never trigger?
Usually the description is too vague. Say what the skill does and include the words people actually type when they need it.