AI engineering

Writing a Claude Code skill for your TYPO3 coding rules

5 min read

Teach the agent your rules

Before I used skills, my prompts to Claude Code kept starting with the same paragraph. “Use ConnectionPool, not raw SQL. Use dependency injection, not makeInstance for services. Site sets, not static includes. PHP 8.2.” Copy, paste, every time. And when the paragraph is missing, you can easily get a ViewHelper with renderStatic(), which was deprecated in TYPO3 13.3, because a lot of older example code still uses it.

Skills fix this. A skill is a folder with a SKILL.md file that Claude Code loads when the task matches its description. Your rules are there when they are needed, and they cost almost nothing when they are not. This post builds one for TYPO3 conventions.

How a skill is loaded

Understanding this one thing makes the rest easy. Claude Code keeps only the name and description of every available skill in its context. The body of SKILL.md is loaded when the skill is invoked, either by Claude because the task matches the description, or by you with /skill-name. Additional files in the folder are read only when the body points to them and the task needs them.

So the description decides whether the skill is used, the body decides how, and the reference files hold the details. Three layers, three different jobs.

Step 1: Create the folder

Project skills live in .claude/skills/<name>/SKILL.md and can be committed, so the whole team gets them. Personal skills live in ~/.claude/skills/.

.claude/skills/typo3-rules/
├── SKILL.md
└── references/
    ├── viewhelpers.md
    ├── database.md
    └── site-sets.md

Step 2: Write a description that triggers

A description like “TYPO3 coding standards” sounds right but rarely triggers. The model does not ask itself “do I need coding standards now?” when somebody says “add a ViewHelper that formats a price”. The description must say what the skill does and when to use it, in words that appear in real requests.

---
name: typo3-rules
description: Project rules for writing TYPO3 v14 extension and site package code (PHP, Fluid, TypoScript, TCA, Services.yaml, site sets). Use whenever you create or change a controller, ViewHelper, repository, event listener, middleware, command, TCA file or TypoScript in this project, even for small edits.
---

Notice the list of concrete things: controller, ViewHelper, repository. Those are the nouns people type. The docs also say to put the key use case first, because a long description is cut in the skill listing.

Step 3: Keep the rules short

The body is not a TYPO3 tutorial. The model knows TYPO3. It needs to know your choices and the places where its training data is older than your project. Mine fits on one screen:

# TYPO3 rules for this project

Target: TYPO3 14.3, PHP 8.2+, Composer mode, DDEV.

## Always
- Constructor injection with `private readonly`; no `GeneralUtility::makeInstance()`
  for services that the container can inject.
- Database access through `ConnectionPool` / QueryBuilder with named parameters.
- Event listeners with `#[AsEventListener]`, not Services.yaml tags.
- TypoScript and TSconfig belong in the site set `Configuration/Sets/Sitepackage/`.
- `declare(strict_types=1);` and native return types everywhere.

## Never
- `$GLOBALS['TSFE']` (removed in v14). Read request attributes instead.
- `renderStatic()` in ViewHelpers (deprecated 13.3). Implement `render()`.
- New hooks in `$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']` when a PSR-14 event exists.

## Before you say done
- Run `ddev composer gate` and show its output.

## Details, read when relevant
- ViewHelpers: [references/viewhelpers.md](references/viewhelpers.md)
- Database and Extbase: [references/database.md](references/database.md)
- Site sets and settings: [references/site-sets.md](references/site-sets.md)

“Always” and “Never” lists work better for me than long prose. Each line is something I have really corrected in AI output before. If a rule never needed a correction, it does not go in.

Step 4: Reference files for the details

Details that matter only for one kind of task go into separate files. Here is a part of references/viewhelpers.md:

# ViewHelpers (TYPO3 14, Fluid 5)

- Extend `TYPO3FluidFluidCoreViewHelperAbstractViewHelper`
  (or `AbstractTagBasedViewHelper` for HTML tags).
- Register arguments in `initializeArguments(): void`; Fluid 5 validates types strictly.
- Declare the return type of `render()`.
- Need the request? `$this->renderingContext->getAttribute(ServerRequestInterface::class)`,
  check `hasAttribute()` first. `getRequest()` does not exist any more.
- Output is escaped by default. Set `$escapeOutput = false` only for trusted HTML and
  say why in a comment.
- Add a functional or unit test next to it.

Every line here has a source I can point to: the Fluid 5 breaking change entries in the TYPO3 changelog and the deprecation of RenderingContext::getRequest(). When a rule changes with the next TYPO3 version, I change one file.

Step 5: Test it on a real task

A skill that does not trigger is just a Markdown file. So I test it in a fresh session, with a request that does not mention the skill:

Add a ViewHelper my:price that formats an integer amount in cent as a price
in the current site language, e.g. 1999 -> "19,99 €" for German.

What I check, in this order:

  1. Did the skill load? Claude Code shows when a skill is used. If it did not load, the description is the problem, not the rules.
  2. Did it open the right reference file? For this task viewhelpers.md should be read, database.md not.
  3. Is the code following the rules? render(): string, typed arguments, the language read from the request attribute, no renderStatic().
  4. Did it run the gate and show the output before saying it is finished?

When one of these fails, I change one thing and run the same request again. Most of the time the description is the problem. Sometimes a rule is ambiguous: “avoid makeInstance” alone does not say what to do with a value object, where new or makeInstance() is perfectly fine. That is why my rule says “for services that the container can inject”.

You can also call the skill by hand with /typo3-rules at the start of a message. That is useful when you want it for sure, but I treat it as a fallback. A good description should make it unnecessary.

Mistakes to avoid

  • Too long. Pasting a whole internal guideline document into SKILL.md is tempting. It loads, and then the important rules drown. The docs recommend keeping SKILL.md under 500 lines; I aim for one screen.
  • Rules without reasons. “Never use TSFE” is weaker than “Never use TSFE (removed in v14)”. The reason helps the model with cases the rule does not name.
  • Version-free rules. Write the TYPO3 version into the skill. A rule that is correct for v14 can be wrong for an extension that still supports v12.

A skill is not magic, but it is the cheapest way I found to make an agent write code the way my team expects. If this post helped you, I will be really happy.

Further reading

Leave a comment

Your email address will not be published.